7 Commits
Author SHA1 Message Date
josh d0f542cbd7 v1.3.0 2024-04-23 00:31:56 -04:00
josh 786c78b635 Update CHANGELOG for v1.3.0 2024-04-23 00:21:28 -04:00
josh f0bd8d8663 Add documentation for AST generation mode - close #22 2024-04-23 00:15:19 -04:00
josh c7a18ef821 Add AST node field name with no suffix when unique - #22 2024-04-22 21:50:26 -04:00
josh cb06a56f81 Add AST generation - #22 2024-04-22 20:51:27 -04:00
josh 2b28ef622d Add specs to fully cover cli.rb 2024-04-06 14:37:15 -04:00
josh 19c32b58dc Fix README example grammar 2024-04-06 14:16:27 -04:00
16 changed files with 831 additions and 109 deletions

No files matched your search

+6
View File
@@ -1,3 +1,9 @@
## v1.3.0
### New Features
- Add AST generation (#22)
## v1.2.0 ## v1.2.0
### New Features ### New Features
+18 -32
View File
@@ -6,7 +6,8 @@ Propane is a LALR Parser Generator (LPG) which:
* generates a built-in lexer to tokenize input * generates a built-in lexer to tokenize input
* supports UTF-8 lexer inputs * supports UTF-8 lexer inputs
* generates a table-driven shift/reduce parser to parse input in linear time * generates a table-driven shift/reduce parser to parse input in linear time
* target C or D language outputs * targets C or D language outputs
* optionally supports automatic full AST generation
* is MIT-licensed * is MIT-licensed
* is distributable as a standalone Ruby script * is distributable as a standalone Ruby script
@@ -17,6 +18,7 @@ can be copied into and versioned in a project's source tree.
The only requirement to run Propane is that the system has a Ruby interpreter The only requirement to run Propane is that the system has a Ruby interpreter
installed. installed.
The latest release can be downloaded from [https://github.com/holtrop/propane/releases](https://github.com/holtrop/propane/releases). The latest release can be downloaded from [https://github.com/holtrop/propane/releases](https://github.com/holtrop/propane/releases).
Simply copy the `propane` executable script into the desired location within Simply copy the `propane` executable script into the desired location within
the project to be built (typically the root of the repository) and mark it the project to be built (typically the root of the repository) and mark it
executable. executable.
@@ -55,10 +57,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)
{ {
@@ -67,38 +69,22 @@ token integer /\\d+/ <<
} }
$$ = v; $$ = v;
>> >>
token lparen /\\(/; token lparen /\(/;
token rparen /\\)/; token rparen /\)/;
# Drop whitespace. # Drop whitespace.
drop /\\s+/; drop /\s+/;
Start -> E1 << Start -> E1 << $$ = $1; >>
$$ = $1; E1 -> E2 << $$ = $1; >>
>> E1 -> E1 plus E2 << $$ = $1 + $3; >>
E1 -> E2 << E2 -> E3 << $$ = $1; >>
$$ = $1; E2 -> E2 times E3 << $$ = $1 * $3; >>
>> E3 -> E4 << $$ = $1; >>
E1 -> E1 plus E2 <<
$$ = $1 + $3;
>>
E2 -> E3 <<
$$ = $1;
>>
E2 -> E2 times E3 <<
$$ = $1 * $3;
>>
E3 -> E4 <<
$$ = $1;
>>
E3 -> E3 power E4 << E3 -> E3 power E4 <<
$$ = pow($1, $3); $$ = pow($1, $3);
>> >>
E4 -> integer << E4 -> integer << $$ = $1; >>
$$ = $1; E4 -> lparen E1 rparen << $$ = $2; >>
>>
E4 -> lparen E1 rparen <<
$$ = $2;
>>
``` ```
Grammar files can contain comment lines beginning with `#` which are ignored. Grammar files can contain comment lines beginning with `#` which are ignored.
+100 -8
View File
@@ -320,9 +320,12 @@ static lexer_state_id_t check_lexer_transition(uint32_t current_state, uint32_t
* *
* @param context * @param context
* Lexer/parser context structure. * Lexer/parser context structure.
* @param[out] out_token_info * @param[out] out_match_info
* The lexed token information is stored here if the return value is * The longest match information is stored here if the return value is
* P_SUCCESS. * P_SUCCESS or P_DECODE_ERROR.
* @param[out] out_unexpected_input_length
* The unexpected input length is stored here if the return value is
* P_UNEXPECTED_INPUT.
* *
* @reval P_SUCCESS * @reval P_SUCCESS
* A token was successfully lexed. * A token was successfully lexed.
@@ -608,6 +611,18 @@ typedef struct
* reduce action. * reduce action.
*/ */
parser_state_id_t n_states; parser_state_id_t n_states;
<% if @grammar.ast %>
/**
* Map of rule components to rule set child fields.
*/
uint16_t const * rule_set_node_field_index_map;
/**
* Number of rule set AST node fields.
*/
uint16_t rule_set_node_field_array_size;
<% end %>
} reduce_t; } reduce_t;
/** Parser state entry. */ /** Parser state entry. */
@@ -638,6 +653,11 @@ typedef struct
/** Parser value from this state. */ /** Parser value from this state. */
<%= @grammar.prefix %>value_t pvalue; <%= @grammar.prefix %>value_t pvalue;
<% if @grammar.ast %>
/** AST node. */
void * ast_node;
<% end %>
} state_value_t; } state_value_t;
/** Parser shift table. */ /** Parser shift table. */
@@ -647,10 +667,27 @@ static const shift_t parser_shift_table[] = {
<% end %> <% end %>
}; };
<% if @grammar.ast %>
<% @grammar.rules.each do |rule| %>
<% unless rule.flat_rule_set_node_field_index_map? %>
const uint16_t r_<%= rule.name.gsub("$", "_") %><%= rule.id %>_node_field_index_map[<%= rule.rule_set_node_field_index_map.size %>] = {<%= rule.rule_set_node_field_index_map.map {|v| v.to_s}.join(", ") %>};
<% end %>
<% end %>
<% end %>
/** Parser reduce table. */ /** Parser reduce table. */
static const reduce_t parser_reduce_table[] = { static const reduce_t parser_reduce_table[] = {
<% @parser.reduce_table.each do |reduce| %> <% @parser.reduce_table.each do |reduce| %>
{<%= reduce[:token_id] %>u, <%= reduce[:rule_id] %>u, <%= reduce[:rule_set_id] %>u, <%= reduce[:n_states] %>u}, {<%= reduce[:token_id] %>u, <%= reduce[:rule_id] %>u, <%= reduce[:rule_set_id] %>u, <%= reduce[:n_states] %>u
<% if @grammar.ast %>
<% if reduce[:rule].flat_rule_set_node_field_index_map? %>
, NULL
<% else %>
, &r_<%= reduce[:rule].name.gsub("$", "_") %><%= reduce[:rule].id %>_node_field_index_map[0]
<% end %>
, <%= reduce[:rule].rule_set.ast_fields.size %>
<% end %>
},
<% end %> <% end %>
}; };
@@ -754,6 +791,7 @@ static void state_values_stack_free(state_values_stack_t * stack)
free(stack->entries); free(stack->entries);
} }
<% unless @grammar.ast %>
/** /**
* Execute user code associated with a parser rule. * Execute user code associated with a parser rule.
* *
@@ -768,18 +806,19 @@ static size_t parser_user_code(<%= @grammar.prefix %>value_t * _pvalue, uint32_t
{ {
switch (rule) switch (rule)
{ {
<% @grammar.rules.each do |rule| %> <% @grammar.rules.each do |rule| %>
<% if rule.code %> <% if rule.code %>
case <%= rule.id %>u: { case <%= rule.id %>u: {
<%= expand_code(rule.code, true, rule, nil) %> <%= expand_code(rule.code, true, rule, nil) %>
} break; } break;
<% end %>
<% end %> <% end %>
<% end %>
default: break; default: break;
} }
return P_SUCCESS; return P_SUCCESS;
} }
<% end %>
/** /**
* Check if the parser should shift to a new state. * Check if the parser should shift to a new state.
@@ -853,7 +892,11 @@ size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context)
<%= @grammar.prefix %>token_t token = INVALID_TOKEN_ID; <%= @grammar.prefix %>token_t token = INVALID_TOKEN_ID;
state_values_stack_t statevalues; state_values_stack_t statevalues;
size_t reduced_rule_set = INVALID_ID; size_t reduced_rule_set = INVALID_ID;
<% if @grammar.ast %>
void * reduced_parser_node;
<% else %>
<%= @grammar.prefix %>value_t reduced_parser_value; <%= @grammar.prefix %>value_t reduced_parser_value;
<% end %>
state_values_stack_init(&statevalues); state_values_stack_init(&statevalues);
state_values_stack_push(&statevalues); state_values_stack_push(&statevalues);
size_t result; size_t result;
@@ -880,7 +923,11 @@ size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context)
if ((shift_state != INVALID_ID) && (token == TOKEN___EOF)) if ((shift_state != INVALID_ID) && (token == TOKEN___EOF))
{ {
/* Successful parse. */ /* Successful parse. */
<% if @grammar.ast %>
context->parse_result = (Start *)state_values_stack_index(&statevalues, -1)->ast_node;
<% else %>
context->parse_result = state_values_stack_index(&statevalues, -1)->pvalue; context->parse_result = state_values_stack_index(&statevalues, -1)->pvalue;
<% end %>
result = P_SUCCESS; result = P_SUCCESS;
break; break;
} }
@@ -893,15 +940,26 @@ size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context)
if (reduced_rule_set == INVALID_ID) if (reduced_rule_set == INVALID_ID)
{ {
/* We shifted a token, mark it consumed. */ /* We shifted a token, mark it consumed. */
token = INVALID_TOKEN_ID; <% if @grammar.ast %>
Token * token_ast_node = malloc(sizeof(Token));
token_ast_node->token = token;
token_ast_node->pvalue = token_info.pvalue;
state_values_stack_index(&statevalues, -1)->ast_node = token_ast_node;
<% else %>
state_values_stack_index(&statevalues, -1)->pvalue = token_info.pvalue; state_values_stack_index(&statevalues, -1)->pvalue = token_info.pvalue;
<% end %>
token = INVALID_TOKEN_ID;
} }
else else
{ {
/* We shifted a RuleSet. */ /* We shifted a RuleSet. */
<% if @grammar.ast %>
state_values_stack_index(&statevalues, -1)->ast_node = reduced_parser_node;
<% 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 = {0};
reduced_parser_value = new_parse_result; reduced_parser_value = new_parse_result;
<% end %>
reduced_rule_set = INVALID_ID; reduced_rule_set = INVALID_ID;
} }
continue; continue;
@@ -911,12 +969,38 @@ size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context)
if (reduce_index != INVALID_ID) if (reduce_index != INVALID_ID)
{ {
/* We have something to reduce. */ /* We have something to reduce. */
<% if @grammar.ast %>
if (parser_reduce_table[reduce_index].n_states > 0)
{
void ** node_fields = calloc(parser_reduce_table[reduce_index].rule_set_node_field_array_size, sizeof(void *));
if (parser_reduce_table[reduce_index].rule_set_node_field_index_map == NULL)
{
for (size_t i = 0; i < parser_reduce_table[reduce_index].n_states; i++)
{
node_fields[i] = state_values_stack_index(&statevalues, -(int)parser_reduce_table[reduce_index].n_states + (int)i)->ast_node;
}
}
else
{
for (size_t i = 0; i < parser_reduce_table[reduce_index].n_states; i++)
{
node_fields[parser_reduce_table[reduce_index].rule_set_node_field_index_map[i]] = state_values_stack_index(&statevalues, -(int)parser_reduce_table[reduce_index].n_states + (int)i)->ast_node;
}
}
reduced_parser_node = node_fields;
}
else
{
reduced_parser_node = NULL;
}
<% else %>
<%= @grammar.prefix %>value_t reduced_parser_value2 = {0}; <%= @grammar.prefix %>value_t reduced_parser_value2 = {0};
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;
} }
reduced_parser_value = reduced_parser_value2; reduced_parser_value = reduced_parser_value2;
<% end %>
reduced_rule_set = parser_reduce_table[reduce_index].rule_set; reduced_rule_set = parser_reduce_table[reduce_index].rule_set;
state_values_stack_pop(&statevalues, parser_reduce_table[reduce_index].n_states); state_values_stack_pop(&statevalues, parser_reduce_table[reduce_index].n_states);
continue; continue;
@@ -944,9 +1028,17 @@ size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context)
* *
* @return Parse result value. * @return Parse result value.
*/ */
<% if @grammar.ast %>
Start * <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context)
<% else %>
<%= start_rule_type[1] %> <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context) <%= start_rule_type[1] %> <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context)
<% end %>
{ {
<% if @grammar.ast %>
return context->parse_result;
<% else %>
return context->parse_result.v_<%= start_rule_type[0] %>; return context->parse_result.v_<%= start_rule_type[0] %>;
<% end %>
} }
/** /**
+139 -10
View File
@@ -49,13 +49,44 @@ public enum : <%= @grammar.prefix %>token_t
/** Code point type. */ /** Code point type. */
public alias <%= @grammar.prefix %>code_point_t = uint; public alias <%= @grammar.prefix %>code_point_t = uint;
<% if @grammar.ast %>
/** Parser values type. */
public alias <%= @grammar.prefix %>value_t = <%= @grammar.ptype %>;
<% else %>
/** Parser values type(s). */ /** Parser values type(s). */
public union <%= @grammar.prefix %>value_t public union <%= @grammar.prefix %>value_t
{ {
<% @grammar.ptypes.each do |name, typestring| %> <% @grammar.ptypes.each do |name, typestring| %>
<%= typestring %> v_<%= name %>; <%= typestring %> v_<%= name %>;
<% end %> <% end %>
} }
<% end %>
<% if @grammar.ast %>
/** AST node types. @{ */
public struct Token
{
<%= @grammar.prefix %>token_t token;
<%= @grammar.prefix %>value_t pvalue;
}
<% @parser.rule_sets.each do |name, rule_set| %>
<% next if name.start_with?("$") %>
public struct <%= name %>
{
<% rule_set.ast_fields.each do |fields| %>
union
{
<% fields.each do |field_name, type| %>
<%= type %> * <%= field_name %>;
<% end %>
}
<% end %>
}
<% end %>
/** @} */
<% end %>
/** /**
* A structure to keep track of parser position. * A structure to keep track of parser position.
@@ -112,7 +143,11 @@ public struct <%= @grammar.prefix %>context_t
/* Parser context data. */ /* Parser context data. */
/** Parse result value. */ /** Parse result value. */
<% if @grammar.ast %>
Start * parse_result;
<% else %>
<%= @grammar.prefix %>value_t parse_result; <%= @grammar.prefix %>value_t parse_result;
<% end %>
/** Unexpected token received. */ /** Unexpected token received. */
<%= @grammar.prefix %>token_t token; <%= @grammar.prefix %>token_t token;
@@ -428,9 +463,12 @@ private lexer_state_id_t check_lexer_transition(uint current_state, uint code_po
* *
* @param context * @param context
* Lexer/parser context structure. * Lexer/parser context structure.
* @param[out] out_token_info * @param[out] out_match_info
* The lexed token information is stored here if the return value is * The longest match information is stored here if the return value is
* P_SUCCESS. * P_SUCCESS or P_DECODE_ERROR.
* @param[out] out_unexpected_input_length
* The unexpected input length is stored here if the return value is
* P_UNEXPECTED_INPUT.
* *
* @reval P_SUCCESS * @reval P_SUCCESS
* A token was successfully lexed. * A token was successfully lexed.
@@ -714,6 +752,18 @@ private struct reduce_t
* reduce action. * reduce action.
*/ */
parser_state_id_t n_states; parser_state_id_t n_states;
<% if @grammar.ast %>
/**
* Map of rule components to rule set child fields.
*/
immutable(ushort) * rule_set_node_field_index_map;
/**
* Number of rule set AST node fields.
*/
ushort rule_set_node_field_array_size;
<% end %>
} }
/** Parser state entry. */ /** Parser state entry. */
@@ -745,6 +795,11 @@ private struct state_value_t
/** Parser value from this state. */ /** Parser value from this state. */
<%= @grammar.prefix %>value_t pvalue; <%= @grammar.prefix %>value_t pvalue;
<% if @grammar.ast %>
/** AST node. */
void * ast_node;
<% end %>
this(size_t state_id) this(size_t state_id)
{ {
this.state_id = state_id; this.state_id = state_id;
@@ -758,10 +813,27 @@ private immutable shift_t[] parser_shift_table = [
<% end %> <% end %>
]; ];
<% if @grammar.ast %>
<% @grammar.rules.each do |rule| %>
<% unless rule.flat_rule_set_node_field_index_map? %>
immutable ushort[<%= rule.rule_set_node_field_index_map.size %>] r_<%= rule.name.gsub("$", "_") %><%= rule.id %>_node_field_index_map = [<%= rule.rule_set_node_field_index_map.map {|v| v.to_s}.join(", ") %>];
<% end %>
<% end %>
<% end %>
/** Parser reduce table. */ /** Parser reduce table. */
private immutable reduce_t[] parser_reduce_table = [ private immutable reduce_t[] parser_reduce_table = [
<% @parser.reduce_table.each do |reduce| %> <% @parser.reduce_table.each do |reduce| %>
reduce_t(<%= reduce[:token_id] %>u, <%= reduce[:rule_id] %>u, <%= reduce[:rule_set_id] %>u, <%= reduce[:n_states] %>u), reduce_t(<%= reduce[:token_id] %>u, <%= reduce[:rule_id] %>u, <%= reduce[:rule_set_id] %>u, <%= reduce[:n_states] %>u
<% if @grammar.ast %>
<% if reduce[:rule].flat_rule_set_node_field_index_map? %>
, null
<% else %>
, &r_<%= reduce[:rule].name.gsub("$", "_") %><%= reduce[:rule].id %>_node_field_index_map[0]
<% end %>
, <%= reduce[:rule].rule_set.ast_fields.size %>
<% end %>
),
<% end %> <% end %>
]; ];
@@ -772,6 +844,7 @@ private immutable parser_state_t[] parser_state_table = [
<% end %> <% end %>
]; ];
<% unless @grammar.ast %>
/** /**
* Execute user code associated with a parser rule. * Execute user code associated with a parser rule.
* *
@@ -786,18 +859,19 @@ private size_t parser_user_code(<%= @grammar.prefix %>value_t * _pvalue, uint ru
{ {
switch (rule) switch (rule)
{ {
<% @grammar.rules.each do |rule| %> <% @grammar.rules.each do |rule| %>
<% if rule.code %> <% if rule.code %>
case <%= rule.id %>u: { case <%= rule.id %>u: {
<%= expand_code(rule.code, true, rule, nil) %> <%= expand_code(rule.code, true, rule, nil) %>
} break; } break;
<% end %>
<% end %> <% end %>
<% end %>
default: break; default: break;
} }
return P_SUCCESS; return P_SUCCESS;
} }
<% end %>
/** /**
* Check if the parser should shift to a new state. * Check if the parser should shift to a new state.
@@ -871,7 +945,11 @@ public size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * cont
<%= @grammar.prefix %>token_t token = INVALID_TOKEN_ID; <%= @grammar.prefix %>token_t token = INVALID_TOKEN_ID;
state_value_t[] statevalues = new state_value_t[](1); state_value_t[] statevalues = new state_value_t[](1);
size_t reduced_rule_set = INVALID_ID; size_t reduced_rule_set = INVALID_ID;
<% if @grammar.ast %>
void * reduced_parser_node;
<% else %>
<%= @grammar.prefix %>value_t reduced_parser_value; <%= @grammar.prefix %>value_t reduced_parser_value;
<% end %>
for (;;) for (;;)
{ {
if (token == INVALID_TOKEN_ID) if (token == INVALID_TOKEN_ID)
@@ -894,7 +972,11 @@ public size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * cont
if ((shift_state != INVALID_ID) && (token == TOKEN___EOF)) if ((shift_state != INVALID_ID) && (token == TOKEN___EOF))
{ {
/* Successful parse. */ /* Successful parse. */
<% if @grammar.ast %>
context.parse_result = cast(Start *)statevalues[$-1].ast_node;
<% else %>
context.parse_result = statevalues[$-1].pvalue; context.parse_result = statevalues[$-1].pvalue;
<% end %>
return P_SUCCESS; return P_SUCCESS;
} }
} }
@@ -905,15 +987,24 @@ public size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * cont
if (reduced_rule_set == INVALID_ID) if (reduced_rule_set == INVALID_ID)
{ {
/* We shifted a token, mark it consumed. */ /* We shifted a token, mark it consumed. */
token = INVALID_TOKEN_ID; <% if @grammar.ast %>
Token * token_ast_node = new Token(token, token_info.pvalue);
statevalues[$-1].ast_node = token_ast_node;
<% else %>
statevalues[$-1].pvalue = token_info.pvalue; statevalues[$-1].pvalue = token_info.pvalue;
<% end %>
token = INVALID_TOKEN_ID;
} }
else else
{ {
/* We shifted a RuleSet. */ /* We shifted a RuleSet. */
<% if @grammar.ast %>
statevalues[$-1].ast_node = reduced_parser_node;
<% else %>
statevalues[$-1].pvalue = reduced_parser_value; statevalues[$-1].pvalue = reduced_parser_value;
<%= @grammar.prefix %>value_t new_parse_result; <%= @grammar.prefix %>value_t new_parse_result;
reduced_parser_value = new_parse_result; reduced_parser_value = new_parse_result;
<% end %>
reduced_rule_set = INVALID_ID; reduced_rule_set = INVALID_ID;
} }
continue; continue;
@@ -923,12 +1014,42 @@ public size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * cont
if (reduce_index != INVALID_ID) if (reduce_index != INVALID_ID)
{ {
/* We have something to reduce. */ /* We have something to reduce. */
<% if @grammar.ast %>
if (parser_reduce_table[reduce_index].n_states > 0)
{
void *[] node_fields = new void *[parser_reduce_table[reduce_index].rule_set_node_field_array_size];
foreach (i; 0..parser_reduce_table[reduce_index].rule_set_node_field_array_size)
{
node_fields[i] = null;
}
if (parser_reduce_table[reduce_index].rule_set_node_field_index_map is null)
{
foreach (i; 0..parser_reduce_table[reduce_index].n_states)
{
node_fields[i] = statevalues[$ - parser_reduce_table[reduce_index].n_states + i].ast_node;
}
}
else
{
foreach (i; 0..parser_reduce_table[reduce_index].n_states)
{
node_fields[parser_reduce_table[reduce_index].rule_set_node_field_index_map[i]] = statevalues[$ - parser_reduce_table[reduce_index].n_states + i].ast_node;
}
}
reduced_parser_node = node_fields.ptr;
}
else
{
reduced_parser_node = null;
}
<% else %>
<%= @grammar.prefix %>value_t reduced_parser_value2; <%= @grammar.prefix %>value_t 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;
} }
reduced_parser_value = reduced_parser_value2; reduced_parser_value = reduced_parser_value2;
<% end %>
reduced_rule_set = parser_reduce_table[reduce_index].rule_set; reduced_rule_set = parser_reduce_table[reduce_index].rule_set;
statevalues.length -= parser_reduce_table[reduce_index].n_states; statevalues.length -= parser_reduce_table[reduce_index].n_states;
continue; continue;
@@ -953,9 +1074,17 @@ public size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * cont
* *
* @return Parse result value. * @return Parse result value.
*/ */
<% if @grammar.ast %>
public Start * <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context)
<% else %>
public <%= start_rule_type[1] %> <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context) public <%= start_rule_type[1] %> <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context)
<% end %>
{ {
<% if @grammar.ast %>
return context.parse_result;
<% else %>
return context.parse_result.v_<%= start_rule_type[0] %>; return context.parse_result.v_<%= start_rule_type[0] %>;
<% end %>
} }
/** /**
+46 -2
View File
@@ -41,13 +41,49 @@ typedef uint32_t <%= @grammar.prefix %>code_point_t;
/** User header code blocks. */ /** User header code blocks. */
<%= @grammar.code_blocks.fetch("header", "") %> <%= @grammar.code_blocks.fetch("header", "") %>
<% if @grammar.ast %>
/** Parser values type. */
typedef <%= @grammar.ptype %> <%= @grammar.prefix %>value_t;
<% else %>
/** Parser values type(s). */ /** Parser values type(s). */
typedef union typedef union
{ {
<% @grammar.ptypes.each do |name, typestring| %> <% @grammar.ptypes.each do |name, typestring| %>
<%= typestring %> v_<%= name %>; <%= typestring %> v_<%= name %>;
<% end %> <% end %>
} <%= @grammar.prefix %>value_t; } <%= @grammar.prefix %>value_t;
<% end %>
<% if @grammar.ast %>
/** AST node types. @{ */
typedef struct Token
{
<%= @grammar.prefix %>token_t token;
<%= @grammar.prefix %>value_t pvalue;
} Token;
<% @parser.rule_sets.each do |name, rule_set| %>
<% next if name.start_with?("$") %>
struct <%= name %>;
<% end %>
<% @parser.rule_sets.each do |name, rule_set| %>
<% next if name.start_with?("$") %>
typedef struct <%= name %>
{
<% rule_set.ast_fields.each do |fields| %>
union
{
<% fields.each do |field_name, type| %>
struct <%= type %> * <%= field_name %>;
<% end %>
};
<% end %>
} <%= name %>;
<% end %>
/** @} */
<% end %>
/** /**
* A structure to keep track of parser position. * A structure to keep track of parser position.
@@ -107,7 +143,11 @@ typedef struct
/* Parser context data. */ /* Parser context data. */
/** Parse result value. */ /** Parse result value. */
<% if @grammar.ast %>
Start * parse_result;
<% else %>
<%= @grammar.prefix %>value_t parse_result; <%= @grammar.prefix %>value_t parse_result;
<% end %>
/** Unexpected token received. */ /** Unexpected token received. */
<%= @grammar.prefix %>token_t token; <%= @grammar.prefix %>token_t token;
@@ -132,7 +172,11 @@ size_t <%= @grammar.prefix %>lex(<%= @grammar.prefix %>context_t * context, <%=
size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context); size_t <%= @grammar.prefix %>parse(<%= @grammar.prefix %>context_t * context);
<% if @grammar.ast %>
Start * <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context);
<% else %>
<%= start_rule_type[1] %> <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context); <%= start_rule_type[1] %> <%= @grammar.prefix %>result(<%= @grammar.prefix %>context_t * context);
<% end %>
<%= @grammar.prefix %>position_t <%= @grammar.prefix %>position(<%= @grammar.prefix %>context_t * context); <%= @grammar.prefix %>position_t <%= @grammar.prefix %>position(<%= @grammar.prefix %>context_t * context);
+167
View File
@@ -14,6 +14,7 @@ Propane is a LALR Parser Generator (LPG) which:
* supports UTF-8 lexer inputs * supports UTF-8 lexer inputs
* generates a table-driven shift/reduce parser to parse input in linear time * generates a table-driven shift/reduce parser to parse input in linear time
* targets C or D language outputs * targets C or D language outputs
* optionally supports automatic full AST generation
* is MIT-licensed * is MIT-licensed
* is distributable as a standalone Ruby script * is distributable as a standalone Ruby script
@@ -182,6 +183,99 @@ rule.
Parser values for the rules or tokens in the rule pattern can be accessed Parser values for the rules or tokens in the rule pattern can be accessed
positionally with tokens `$1`, `$2`, `$3`, etc... positionally with tokens `$1`, `$2`, `$3`, etc...
Parser rule code blocks are not available in AST generation mode.
In AST generation mode, a full parse tree is automatically constructed in
memory for user code to traverse after parsing is complete.
##> AST generation mode - the `ast` statement
To activate AST generation mode, place the `ast` statement in your grammar file:
```
ast;
```
It is recommended to place this statement early in the grammar.
In AST generation mode various aspects of propane's behavior are changed:
* Only one `ptype` is allowed.
* Parser user code blocks are not supported.
* Structure types are generated to represent the parsed tokens and rules as
defined in the grammar.
* The parse result from `p_result()` points to a `Start` structure containing
the entire parse tree for the input.
Example AST generation grammar:
```
ast;
ptype int;
token a << $$ = 11; >>
token b << $$ = 22; >>
token one /1/;
token two /2/;
token comma /,/ <<
$$ = 42;
>>
token lparen /\\(/;
token rparen /\\)/;
drop /\\s+/;
Start -> Items;
Items -> Item ItemsMore;
Items -> ;
ItemsMore -> comma Item ItemsMore;
ItemsMore -> ;
Item -> a;
Item -> b;
Item -> lparen Item rparen;
Item -> Dual;
Dual -> One Two;
Dual -> Two One;
One -> one;
Two -> two;
```
The following unit test describes the fields that will be present for an
example parse:
```
string input = "a, ((b)), b";
p_context_t context;
p_context_init(&context, input);
assert_eq(P_SUCCESS, p_parse(&context));
Start * start = p_result(&context);
assert(start.pItems1 !is null);
assert(start.pItems !is null);
Items * items = start.pItems;
assert(items.pItem !is null);
assert(items.pItem.pToken1 !is null);
assert_eq(TOKEN_a, items.pItem.pToken1.token);
assert_eq(11, items.pItem.pToken1.pvalue);
assert(items.pItemsMore !is null);
ItemsMore * itemsmore = items.pItemsMore;
assert(itemsmore.pItem !is null);
assert(itemsmore.pItem.pItem !is null);
assert(itemsmore.pItem.pItem.pItem !is null);
assert(itemsmore.pItem.pItem.pItem.pToken1 !is null);
assert_eq(TOKEN_b, itemsmore.pItem.pItem.pItem.pToken1.token);
assert_eq(22, itemsmore.pItem.pItem.pItem.pToken1.pvalue);
assert(itemsmore.pItemsMore !is null);
itemsmore = itemsmore.pItemsMore;
assert(itemsmore.pItem !is null);
assert(itemsmore.pItem.pToken1 !is null);
assert_eq(TOKEN_b, itemsmore.pItem.pToken1.token);
assert_eq(22, itemsmore.pItem.pToken1.pvalue);
assert(itemsmore.pItemsMore is null);
```
##> Specifying tokens - the `token` statement ##> Specifying tokens - the `token` statement
The `token` statement allows defining a lexer token and a pattern to match that The `token` statement allows defining a lexer token and a pattern to match that
@@ -442,6 +536,12 @@ In this example:
* a reduced `Values`'s parser value has a type of `Value[]`. * a reduced `Values`'s parser value has a type of `Value[]`.
* a reduced `KeyValue`'s parser value has a type of `Value[string]`. * a reduced `KeyValue`'s parser value has a type of `Value[string]`.
When AST generation mode is active, the `ptype` functionality works differently.
In this mode, only one `ptype` is used by the parser.
Lexer user code blocks may assign a parse value to the generated `Token` node
by assigning to `$$` within a lexer code block.
The type of the parse value `$$` is given by the global `ptype` type.
##> Specifying a parser rule - the rule statement ##> Specifying a parser rule - the rule statement
Rule statements create parser rules which define the grammar that will be Rule statements create parser rules which define the grammar that will be
@@ -490,6 +590,9 @@ The `$$` symbol accesses the output parser value for this rule.
The above examples demonstrate how the parser values for the rule components The above examples demonstrate how the parser values for the rule components
can be used to produce the parser value for the accepted rule. can be used to produce the parser value for the accepted rule.
Parser rule code blocks are not allowed and not used when AST generation mode
is active.
##> Specifying the parser module name - the `module` statement ##> Specifying the parser module name - the `module` statement
The `module` statement can be used to specify the module name for a generated The `module` statement can be used to specify the module name for a generated
@@ -586,6 +689,67 @@ A pointer to this instance is passed to the generated functions.
The `p_position_t` structure contains two fields `row` and `col`. The `p_position_t` structure contains two fields `row` and `col`.
These fields contain the 0-based row and column describing a parser position. These fields contain the 0-based row and column describing a parser position.
### AST Node Types
If AST generation mode is enabled, a structure type for each rule will be
generated.
The name of the structure type is given by the name of the rule.
Additionally a structure type called `Token` is generated to represent an
AST node which refers to a raw parser token rather than a composite rule.
#### AST Node Fields
A `Token` node has two fields:
* `token` which specifies which token was parsed (one of `TOKEN_*`)
* `pvalue` which specifies the parser value for the token. If a lexer user
code block assigned to `$$`, the assigned value will be stored here.
The other generated AST node structures have fields generated based on the
right hand side components specified for all rules of a given name.
In this example:
```
Start -> Items;
Items -> Item ItemsMore;
Items -> ;
```
The `Start` structure will have a field called `pItems` and another field of
the same name but with a positional suffix (`pItems1`) which both point to the
parsed `Items` node.
Their value will be null if the parsed `Items` rule was empty.
The `Items` structure will have fields:
* `pItem` and `pItem1` which point to the parsed `Item` structure.
* `pItemsMore` and `pItemsMore2` which point to the parsed `ItemsMore` structure.
If a rule can be empty (for example in the second `Items` rule above), then
an instance of a pointer to that rule's generated AST node will be null if the
parser matches the empty rule definition.
The non-positional AST node field pointer will not be generated if there are
multiple positions in which an instance of the node it points to could be
present.
For example, in the below rules:
```
Dual -> One Two;
Dual -> Two One;
```
The generated `Dual` structure will contain `pOne1`, `pTwo2`, `pTwo1`, and
`pOne2` fields.
However, a `pOne` field and `pTwo` field will not be generated since it would
be ambiguous which one was matched.
If the first rule is matched, then `pOne1` and `pTwo2` will be non-null while
`pTwo1` and `pOne2` will be null.
If the second rule is matched instead, then the opposite would be the case.
##> Functions ##> Functions
### `p_context_init` ### `p_context_init`
@@ -639,6 +803,9 @@ if (p_parse(&context) == P_SUCCESS)
} }
``` ```
If AST generation mode is active, then the `p_result()` function returns a
`Start *` pointing to the `Start` AST structure.
### `p_position` ### `p_position`
The `p_position()` function can be used to retrieve the parser position where The `p_position()` function can be used to retrieve the parser position where
+1 -1
View File
@@ -24,7 +24,7 @@ EOF
log_file = args[i] log_file = args[i]
end end
when "--version" when "--version"
puts "propane v#{VERSION}" puts "propane version #{VERSION}"
return 0 return 0
when "-h", "--help" when "-h", "--help"
puts USAGE puts USAGE
+17 -5
View File
@@ -119,6 +119,9 @@ class Propane
end end
end end
determine_possibly_empty_rulesets!(rule_sets) determine_possibly_empty_rulesets!(rule_sets)
rule_sets.each do |name, rule_set|
rule_set.finalize
end
# Generate the lexer. # Generate the lexer.
@lexer = Lexer.new(@grammar) @lexer = Lexer.new(@grammar)
# Generate the parser. # Generate the parser.
@@ -228,11 +231,20 @@ class Propane
end end
else else
code = code.gsub(/\$\$/) do |match| code = code.gsub(/\$\$/) do |match|
case @language if @grammar.ast
when "c" case @language
"out_token_info->pvalue.v_#{pattern.ptypename}" when "c"
when "d" "out_token_info->pvalue"
"out_token_info.pvalue.v_#{pattern.ptypename}" when "d"
"out_token_info.pvalue"
end
else
case @language
when "c"
"out_token_info->pvalue.v_#{pattern.ptypename}"
when "d"
"out_token_info.pvalue.v_#{pattern.ptypename}"
end
end end
end end
code = code.gsub(/\$mode\(([a-zA-Z_][a-zA-Z_0-9]*)\)/) do |match| code = code.gsub(/\$mode\(([a-zA-Z_][a-zA-Z_0-9]*)\)/) do |match|
+31 -3
View File
@@ -5,6 +5,7 @@ class Propane
# Reserve identifiers beginning with a double-underscore for internal use. # Reserve identifiers beginning with a double-underscore for internal use.
IDENTIFIER_REGEX = /(?:[a-zA-Z]|_[a-zA-Z0-9])[a-zA-Z_0-9]*/ IDENTIFIER_REGEX = /(?:[a-zA-Z]|_[a-zA-Z0-9])[a-zA-Z_0-9]*/
attr_reader :ast
attr_reader :modulename attr_reader :modulename
attr_reader :patterns attr_reader :patterns
attr_reader :rules attr_reader :rules
@@ -24,6 +25,7 @@ class Propane
@input = input.gsub("\r\n", "\n") @input = input.gsub("\r\n", "\n")
@ptypes = {"default" => "void *"} @ptypes = {"default" => "void *"}
@prefix = "p_" @prefix = "p_"
@ast = false
parse_grammar! parse_grammar!
end end
@@ -51,6 +53,7 @@ class Propane
if parse_white_space! if parse_white_space!
elsif parse_comment_line! elsif parse_comment_line!
elsif @mode.nil? && parse_mode_label! elsif @mode.nil? && parse_mode_label!
elsif parse_ast_statement!
elsif parse_module_statement! elsif parse_module_statement!
elsif parse_ptype_statement! elsif parse_ptype_statement!
elsif parse_pattern_statement! elsif parse_pattern_statement!
@@ -82,6 +85,12 @@ class Propane
consume!(/#.*\n/) consume!(/#.*\n/)
end end
def parse_ast_statement!
if consume!(/ast\s*;/)
@ast = true
end
end
def parse_module_statement! def parse_module_statement!
if consume!(/module\s+/) if consume!(/module\s+/)
md = consume!(/([\w.]+)\s*/, "expected module name") md = consume!(/([\w.]+)\s*/, "expected module name")
@@ -96,6 +105,9 @@ class Propane
if consume!(/ptype\s+/) if consume!(/ptype\s+/)
name = "default" name = "default"
if md = consume!(/(#{IDENTIFIER_REGEX})\s*=\s*/) if md = consume!(/(#{IDENTIFIER_REGEX})\s*=\s*/)
if @ast
raise Error.new("Multiple ptypes are unsupported in AST mode")
end
name = md[1] name = md[1]
end end
md = consume!(/([^;]+);/, "expected parser result type expression") md = consume!(/([^;]+);/, "expected parser result type expression")
@@ -108,12 +120,15 @@ class Propane
md = consume!(/(#{IDENTIFIER_REGEX})\s*/, "expected token name") md = consume!(/(#{IDENTIFIER_REGEX})\s*/, "expected token name")
name = md[1] name = md[1]
if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/) if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/)
if @ast
raise Error.new("Multiple ptypes are unsupported in AST mode")
end
ptypename = md[1] ptypename = md[1]
end end
pattern = parse_pattern! || name pattern = parse_pattern! || name
consume!(/\s+/) consume!(/\s+/)
unless code = parse_code_block! unless code = parse_code_block!
consume!(/;/, "expected pattern or `;' or code block") consume!(/;/, "expected `;' or code block")
end end
token = Token.new(name, ptypename, @line_number) token = Token.new(name, ptypename, @line_number)
@tokens << token @tokens << token
@@ -129,6 +144,9 @@ class Propane
md = consume!(/(#{IDENTIFIER_REGEX})\s*/, "expected token name") md = consume!(/(#{IDENTIFIER_REGEX})\s*/, "expected token name")
name = md[1] name = md[1]
if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/) if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/)
if @ast
raise Error.new("Multiple ptypes are unsupported in AST mode")
end
ptypename = md[1] ptypename = md[1]
end end
consume!(/;/, "expected `;'"); consume!(/;/, "expected `;'");
@@ -156,10 +174,17 @@ class Propane
def parse_rule_statement! def parse_rule_statement!
if md = consume!(/(#{IDENTIFIER_REGEX})\s*(?:\((#{IDENTIFIER_REGEX})\))?\s*->\s*/) if md = consume!(/(#{IDENTIFIER_REGEX})\s*(?:\((#{IDENTIFIER_REGEX})\))?\s*->\s*/)
rule_name, ptypename = *md[1, 2] rule_name, ptypename = *md[1, 2]
if @ast && ptypename
raise Error.new("Multiple ptypes are unsupported in AST mode")
end
md = consume!(/((?:#{IDENTIFIER_REGEX}\s*)*)\s*/, "expected rule component list") md = consume!(/((?:#{IDENTIFIER_REGEX}\s*)*)\s*/, "expected rule component list")
components = md[1].strip.split(/\s+/) components = md[1].strip.split(/\s+/)
unless code = parse_code_block! if @ast
consume!(/;/, "expected pattern or `;' or code block") consume!(/;/, "expected `;'")
else
unless code = parse_code_block!
consume!(/;/, "expected `;' or code block")
end
end end
@rules << Rule.new(rule_name, components, code, ptypename, @line_number) @rules << Rule.new(rule_name, components, code, ptypename, @line_number)
@mode = nil @mode = nil
@@ -171,6 +196,9 @@ class Propane
if pattern = parse_pattern! if pattern = parse_pattern!
consume!(/\s+/) consume!(/\s+/)
if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/) if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/)
if @ast
raise Error.new("Multiple ptypes are unsupported in AST mode")
end
ptypename = md[1] ptypename = md[1]
end end
unless code = parse_code_block! unless code = parse_code_block!
+2 -2
View File
@@ -63,11 +63,11 @@ class Propane
reduce_entries = reduce_entries =
case ra = item_set.reduce_actions case ra = item_set.reduce_actions
when Rule when Rule
[{token_id: @grammar.invalid_token_id, rule_id: ra.id, [{token_id: @grammar.invalid_token_id, rule_id: ra.id, rule: ra,
rule_set_id: ra.rule_set.id, n_states: ra.components.size}] rule_set_id: ra.rule_set.id, n_states: ra.components.size}]
when Hash when Hash
ra.map do |token, rule| ra.map do |token, rule|
{token_id: token.id, rule_id: rule.id, {token_id: token.id, rule_id: rule.id, rule: rule,
rule_set_id: rule.rule_set.id, n_states: rule.components.size} rule_set_id: rule.rule_set.id, n_states: rule.components.size}
end end
else else
+17
View File
@@ -30,6 +30,11 @@ class Propane
# The RuleSet that this Rule is a part of. # The RuleSet that this Rule is a part of.
attr_accessor :rule_set attr_accessor :rule_set
# @return [Array<Integer>]
# Map this rule's components to their positions in the parent RuleSet's
# node field pointer array. This is used for AST construction.
attr_accessor :rule_set_node_field_index_map
# Construct a Rule. # Construct a Rule.
# #
# @param name [String] # @param name [String]
@@ -45,6 +50,7 @@ class Propane
def initialize(name, components, code, ptypename, line_number) def initialize(name, components, code, ptypename, line_number)
@name = name @name = name
@components = components @components = components
@rule_set_node_field_index_map = components.map {0}
@code = code @code = code
@ptypename = ptypename @ptypename = ptypename
@line_number = line_number @line_number = line_number
@@ -68,6 +74,17 @@ class Propane
"#{@name} -> #{@components.map(&:name).join(" ")}" "#{@name} -> #{@components.map(&:name).join(" ")}"
end end
# Check whether the rule set node field index map is just a 1:1 mapping.
#
# @return [Boolean]
# Boolean indicating whether the rule set node field index map is just a
# 1:1 mapping.
def flat_rule_set_node_field_index_map?
@rule_set_node_field_index_map.each_with_index.all? do |v, i|
v == i
end
end
end end
end end
+51
View File
@@ -1,5 +1,6 @@
class Propane class Propane
# A RuleSet collects all grammar rules of the same name.
class RuleSet class RuleSet
# @return [Integer] # @return [Integer]
@@ -75,6 +76,56 @@ class Propane
@_start_token_set @_start_token_set
end end
# Build the set of AST fields for this RuleSet.
#
# This is an Array of Hashes. Each entry in the Array corresponds to a
# field location in the AST node. The entry is a Hash. It could have one or
# two keys. It will always have the field name with a positional suffix as
# a key. It may also have the field name without the positional suffix if
# that field only exists in one position across all Rules in the RuleSet.
#
# @return [Array<Hash>]
# AST fields.
def ast_fields
@_ast_fields ||=
begin
field_ast_node_indexes = {}
field_indexes_across_all_rules = {}
ast_node_fields = []
@rules.each do |rule|
rule.components.each_with_index do |component, i|
if component.is_a?(Token)
node_name = "Token"
else
node_name = component.name
end
field_name = "p#{node_name}#{i + 1}"
unless field_ast_node_indexes[field_name]
field_ast_node_indexes[field_name] = ast_node_fields.size
ast_node_fields << {field_name => node_name}
end
field_indexes_across_all_rules[node_name] ||= Set.new
field_indexes_across_all_rules[node_name] << field_ast_node_indexes[field_name]
rule.rule_set_node_field_index_map[i] = field_ast_node_indexes[field_name]
end
end
field_indexes_across_all_rules.each do |node_name, indexes_across_all_rules|
if indexes_across_all_rules.size == 1
# If this field was only seen in one position across all rules,
# then add an alias to the positional field name that does not
# include the position.
ast_node_fields[indexes_across_all_rules.first]["p#{node_name}"] = node_name
end
end
ast_node_fields
end
end
# Finalize a RuleSet after adding all Rules to it.
def finalize
ast_fields
end
end end
end end
+1 -1
View File
@@ -1,3 +1,3 @@
class Propane class Propane
VERSION = "1.2.0" VERSION = "1.3.0"
end end
+123 -45
View File
@@ -13,7 +13,7 @@ describe Propane do
File.write("spec/run/testparser#{options[:name]}.propane", grammar) File.write("spec/run/testparser#{options[:name]}.propane", grammar)
end end
def build_parser(options = {}) def run_propane(options = {})
@statics[:build_test_id] ||= 0 @statics[:build_test_id] ||= 0
@statics[:build_test_id] += 1 @statics[:build_test_id] += 1
if ENV["dist_specs"] if ENV["dist_specs"]
@@ -49,7 +49,11 @@ ENV["TERM"] = nil
EOF EOF
end end
end end
command += %W[spec/run/testparser#{options[:name]}.propane spec/run/testparser#{options[:name]}.#{options[:language]} --log spec/run/testparser#{options[:name]}.log] if options[:args]
command += options[:args]
else
command += %W[spec/run/testparser#{options[:name]}.propane spec/run/testparser#{options[:name]}.#{options[:language]} --log spec/run/testparser#{options[:name]}.log]
end
if (options[:capture]) if (options[:capture])
stdout, stderr, status = Open3.capture3(*command) stdout, stderr, status = Open3.capture3(*command)
Results.new(stdout, stderr, status) Results.new(stdout, stderr, status)
@@ -74,7 +78,7 @@ EOF
expect(result).to be_truthy expect(result).to be_truthy
end end
def run def run_test
stdout, stderr, status = Open3.capture3("spec/run/testparser") stdout, stderr, status = Open3.capture3("spec/run/testparser")
File.binwrite("spec/run/.stderr", stderr) File.binwrite("spec/run/.stderr", stderr)
File.binwrite("spec/run/.stdout", stdout) File.binwrite("spec/run/.stdout", stdout)
@@ -112,6 +116,38 @@ EOF
FileUtils.mkdir_p("spec/run") FileUtils.mkdir_p("spec/run")
end end
it "reports its version" do
results = run_propane(args: %w[--version], capture: true)
expect(results.stdout).to match /propane version \d+\.\d+/
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "shows help usage" do
results = run_propane(args: %w[-h], capture: true)
expect(results.stdout).to match /Usage/i
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "errors with unknown option" do
results = run_propane(args: %w[-i], capture: true)
expect(results.stderr).to match /Error: unknown option -i/
expect(results.status).to_not eq 0
end
it "errors when input and output files are not specified" do
results = run_propane(args: [], capture: true)
expect(results.stderr).to match /Error: specify input and output files/
expect(results.status).to_not eq 0
end
it "errors when input file is not readable" do
results = run_propane(args: %w[nope.txt out.d], capture: true)
expect(results.stderr).to match /Error: cannot read nope.txt/
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
@@ -126,9 +162,9 @@ Start -> Foo;
Foo -> int <<>> Foo -> int <<>>
Foo -> plus <<>> Foo -> plus <<>>
EOF EOF
build_parser(language: language) run_propane(language: language)
compile("spec/test_lexer.#{language}", language: language) compile("spec/test_lexer.#{language}", language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -164,9 +200,9 @@ token int /\\d+/ <<
Start -> int << $$ = $1; >> Start -> int << $$ = $1; >>
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_lexer_unknown_character.#{language}", language: language) compile("spec/test_lexer_unknown_character.#{language}", language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -184,7 +220,7 @@ E -> B;
B -> zero; B -> zero;
B -> one; B -> one;
EOF EOF
build_parser(language: language) run_propane(language: language)
end end
it "generates a parser that does basic math - user guide example" do it "generates a parser that does basic math - user guide example" do
@@ -258,9 +294,9 @@ E4 -> integer << $$ = $1; >>
E4 -> lparen E1 rparen << $$ = $2; >> E4 -> lparen E1 rparen << $$ = $2; >>
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_basic_math_grammar.#{language}", language: language) compile("spec/test_basic_math_grammar.#{language}", language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -272,7 +308,7 @@ Start -> E;
E -> one E; E -> one E;
E -> one; E -> one;
EOF EOF
build_parser(language: language) run_propane(language: language)
end end
it "distinguishes between multiple identical rules with lookahead symbol" do it "distinguishes between multiple identical rules with lookahead symbol" do
@@ -284,9 +320,9 @@ Start -> R2 b;
R1 -> a b; R1 -> a b;
R2 -> a b; R2 -> a b;
EOF EOF
build_parser(language: language) run_propane(language: language)
compile("spec/test_parser_identical_rules_lookahead.#{language}", language: language) compile("spec/test_parser_identical_rules_lookahead.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -299,9 +335,9 @@ Start -> a R1;
Start -> b R1; Start -> b R1;
R1 -> b; R1 -> b;
EOF EOF
build_parser(language: language) run_propane(language: language)
compile("spec/test_parser_rule_from_multiple_states.#{language}", language: language) compile("spec/test_parser_rule_from_multiple_states.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -334,9 +370,9 @@ Abcs -> ;
Abcs -> abc Abcs; Abcs -> abc Abcs;
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_user_code.#{language}", language: language) compile("spec/test_user_code.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
verify_lines(results.stdout, [ verify_lines(results.stdout, [
"abc!", "abc!",
@@ -370,9 +406,9 @@ token abc;
Start -> abc; Start -> abc;
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_pattern.#{language}", language: language) compile("spec/test_pattern.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
verify_lines(results.stdout, [ verify_lines(results.stdout, [
"def!", "def!",
@@ -412,9 +448,9 @@ token abc;
Start -> abc; Start -> abc;
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_return_token_from_pattern.#{language}", language: language) compile("spec/test_return_token_from_pattern.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
verify_lines(results.stdout, [ verify_lines(results.stdout, [
"def!", "def!",
@@ -470,9 +506,9 @@ string: /"/ <<
Start -> abc string def; Start -> abc string def;
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_lexer_modes.#{language}", language: language) compile("spec/test_lexer_modes.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
verify_lines(results.stdout, [ verify_lines(results.stdout, [
"begin string mode", "begin string mode",
@@ -509,9 +545,9 @@ A -> a << writeln("A!"); >>
B -> b << writeln("B!"); >> B -> b << writeln("B!"); >>
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_parser_rule_user_code.#{language}", language: language) compile("spec/test_parser_rule_user_code.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
verify_lines(results.stdout, [ verify_lines(results.stdout, [
"A!", "A!",
@@ -528,9 +564,9 @@ Start -> As << $$ = $1; >>
As -> << $$ = 0u; >> As -> << $$ = 0u; >>
As -> As a << $$ = $1 + 1u; >> As -> As a << $$ = $1 + 1u; >>
EOF EOF
build_parser(language: language) run_propane(language: language)
compile("spec/test_parsing_lists.#{language}", language: language) compile("spec/test_parsing_lists.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
end end
@@ -549,7 +585,7 @@ Start -> b E d;
E -> e; E -> e;
F -> e; F -> e;
EOF EOF
results = build_parser(capture: true, language: language) results = run_propane(capture: true, language: language)
expect(results.status).to_not eq 0 expect(results.status).to_not eq 0
expect(results.stderr).to match %r{reduce/reduce conflict.*\(E\).*\(F\)} expect(results.stderr).to match %r{reduce/reduce conflict.*\(E\).*\(F\)}
end end
@@ -581,9 +617,9 @@ token id /[a-zA-Z_][a-zA-Z0-9_]*/ <<
Start -> id; Start -> id;
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_lexer_match_text.#{language}", language: language) compile("spec/test_lexer_match_text.#{language}", language: language)
results = run results = run_test
expect(results.status).to eq 0 expect(results.status).to eq 0
verify_lines(results.stdout, [ verify_lines(results.stdout, [
"Matched token is identifier_123", "Matched token is identifier_123",
@@ -614,9 +650,9 @@ Start -> word <<
>> >>
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_lexer_result_value.#{language}", language: language) compile("spec/test_lexer_result_value.#{language}", language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -629,16 +665,16 @@ drop /\\s+/;
Start -> a num Start; Start -> a num Start;
Start -> a num; Start -> a num;
EOF EOF
build_parser(language: language) run_propane(language: language)
compile("spec/test_error_positions.#{language}", language: language) compile("spec/test_error_positions.#{language}", language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
it "allows creating a JSON parser" do it "allows creating a JSON parser" do
write_grammar(File.read("spec/json_parser.#{language}.propane")) write_grammar(File.read("spec/json_parser.#{language}.propane"))
build_parser(language: language) run_propane(language: language)
compile(["spec/test_parsing_json.#{language}", "spec/json_types.#{language}"], language: language) compile(["spec/test_parsing_json.#{language}", "spec/json_types.#{language}"], language: language)
end end
@@ -650,16 +686,16 @@ token num /\\d+/;
drop /\\s+/; drop /\\s+/;
Start -> a num; Start -> a num;
EOF EOF
build_parser(name: "myp1", language: language) run_propane(name: "myp1", language: language)
write_grammar(<<EOF, name: "myp2") write_grammar(<<EOF, name: "myp2")
prefix myp2_; prefix myp2_;
token b; token b;
token c; token c;
Start -> b c b; Start -> b c b;
EOF EOF
build_parser(name: "myp2", language: language) run_propane(name: "myp2", language: language)
compile("spec/test_multiple_parsers.#{language}", parsers: %w[myp1 myp2], language: language) compile("spec/test_multiple_parsers.#{language}", parsers: %w[myp1 myp2], language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -676,9 +712,9 @@ Any -> a;
Any -> b; Any -> b;
Any -> c; Any -> c;
EOF EOF
build_parser(language: language) run_propane(language: language)
compile("spec/test_user_terminate_lexer.#{language}", language: language) compile("spec/test_user_terminate_lexer.#{language}", language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -694,9 +730,9 @@ Any -> b Any << $terminate(4200); >>
Any -> c Any; Any -> c Any;
Any -> ; Any -> ;
EOF EOF
build_parser(language: language) run_propane(language: language)
compile("spec/test_user_terminate.#{language}", language: language) compile("spec/test_user_terminate.#{language}", language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
end end
@@ -738,9 +774,9 @@ tokenid t;
Start -> t; Start -> t;
EOF EOF
end end
build_parser(language: language) run_propane(language: language)
compile("spec/test_match_backslashes.#{language}", language: language) compile("spec/test_match_backslashes.#{language}", language: language)
results = run results = run_test
expect(results.stderr).to eq "" expect(results.stderr).to eq ""
expect(results.status).to eq 0 expect(results.status).to eq 0
verify_lines(results.stdout, [ verify_lines(results.stdout, [
@@ -765,7 +801,49 @@ Start -> ;
Opt -> two; Opt -> two;
Opt -> ; Opt -> ;
EOF EOF
build_parser(language: language) run_propane(language: language)
end
it "generates an AST" do
write_grammar <<EOF
ast;
ptype int;
token a << $$ = 11; >>
token b << $$ = 22; >>
token one /1/;
token two /2/;
token comma /,/ <<
$$ = 42;
>>
token lparen /\\(/;
token rparen /\\)/;
drop /\\s+/;
Start -> Items;
Items -> Item ItemsMore;
Items -> ;
ItemsMore -> comma Item ItemsMore;
ItemsMore -> ;
Item -> a;
Item -> b;
Item -> lparen Item rparen;
Item -> Dual;
Dual -> One Two;
Dual -> Two One;
One -> one;
Two -> two;
EOF
run_propane(language: language)
compile("spec/test_ast.#{language}", language: language)
results = run_test
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end end
end end
end end
+55
View File
@@ -0,0 +1,55 @@
#include "testparser.h"
#include <assert.h>
#include <string.h>
#include "testutils.h"
int main()
{
char const * input = "a, ((b)), b";
p_context_t context;
p_context_init(&context, (uint8_t const *)input, strlen(input));
assert_eq(P_SUCCESS, p_parse(&context));
Start * start = p_result(&context);
assert(start->pItems1 != NULL);
assert(start->pItems != NULL);
Items * items = start->pItems;
assert(items->pItem != NULL);
assert(items->pItem->pToken1 != NULL);
assert_eq(TOKEN_a, items->pItem->pToken1->token);
assert_eq(11, items->pItem->pToken1->pvalue);
assert(items->pItemsMore != NULL);
ItemsMore * itemsmore = items->pItemsMore;
assert(itemsmore->pItem != NULL);
assert(itemsmore->pItem->pItem != NULL);
assert(itemsmore->pItem->pItem->pItem != NULL);
assert(itemsmore->pItem->pItem->pItem->pToken1 != NULL);
assert_eq(TOKEN_b, itemsmore->pItem->pItem->pItem->pToken1->token);
assert_eq(22, itemsmore->pItem->pItem->pItem->pToken1->pvalue);
assert(itemsmore->pItemsMore != NULL);
itemsmore = itemsmore->pItemsMore;
assert(itemsmore->pItem != NULL);
assert(itemsmore->pItem->pToken1 != NULL);
assert_eq(TOKEN_b, itemsmore->pItem->pToken1->token);
assert_eq(22, itemsmore->pItem->pToken1->pvalue);
assert(itemsmore->pItemsMore == NULL);
input = "";
p_context_init(&context, (uint8_t const *)input, strlen(input));
assert_eq(P_SUCCESS, p_parse(&context));
start = p_result(&context);
assert(start->pItems == NULL);
input = "2 1";
p_context_init(&context, (uint8_t const *)input, strlen(input));
assert_eq(P_SUCCESS, p_parse(&context));
start = p_result(&context);
assert(start->pItems != NULL);
assert(start->pItems->pItem != NULL);
assert(start->pItems->pItem->pDual != NULL);
assert(start->pItems->pItem->pDual->pTwo1 != NULL);
assert(start->pItems->pItem->pDual->pOne2 != NULL);
assert(start->pItems->pItem->pDual->pTwo2 == NULL);
assert(start->pItems->pItem->pDual->pOne1 == NULL);
return 0;
}
+57
View File
@@ -0,0 +1,57 @@
import testparser;
import std.stdio;
import testutils;
int main()
{
return 0;
}
unittest
{
string input = "a, ((b)), b";
p_context_t context;
p_context_init(&context, input);
assert_eq(P_SUCCESS, p_parse(&context));
Start * start = p_result(&context);
assert(start.pItems1 !is null);
assert(start.pItems !is null);
Items * items = start.pItems;
assert(items.pItem !is null);
assert(items.pItem.pToken1 !is null);
assert_eq(TOKEN_a, items.pItem.pToken1.token);
assert_eq(11, items.pItem.pToken1.pvalue);
assert(items.pItemsMore !is null);
ItemsMore * itemsmore = items.pItemsMore;
assert(itemsmore.pItem !is null);
assert(itemsmore.pItem.pItem !is null);
assert(itemsmore.pItem.pItem.pItem !is null);
assert(itemsmore.pItem.pItem.pItem.pToken1 !is null);
assert_eq(TOKEN_b, itemsmore.pItem.pItem.pItem.pToken1.token);
assert_eq(22, itemsmore.pItem.pItem.pItem.pToken1.pvalue);
assert(itemsmore.pItemsMore !is null);
itemsmore = itemsmore.pItemsMore;
assert(itemsmore.pItem !is null);
assert(itemsmore.pItem.pToken1 !is null);
assert_eq(TOKEN_b, itemsmore.pItem.pToken1.token);
assert_eq(22, itemsmore.pItem.pToken1.pvalue);
assert(itemsmore.pItemsMore is null);
input = "";
p_context_init(&context, input);
assert_eq(P_SUCCESS, p_parse(&context));
start = p_result(&context);
assert(start.pItems is null);
input = "2 1";
p_context_init(&context, input);
assert_eq(P_SUCCESS, p_parse(&context));
start = p_result(&context);
assert(start.pItems !is null);
assert(start.pItems.pItem !is null);
assert(start.pItems.pItem.pDual !is null);
assert(start.pItems.pItem.pDual.pTwo1 !is null);
assert(start.pItems.pItem.pDual.pOne2 !is null);
assert(start.pItems.pItem.pDual.pTwo2 is null);
assert(start.pItems.pItem.pDual.pOne1 is null);
}