propane/spec/propane_spec.rb
Josh Holtrop 5fc712c6ee Rework tree generation mode and API
Store tree nodes in congruent, compact arena array.
Define handle types to refer to tree nodes rather than pointers to
structure instances.
Free tree with context.
2026-07-27 20:57:44 -04:00

2241 lines
55 KiB
Ruby

require "fileutils"
require "open3"
Results = Struct.new(:stdout, :stderr, :status)
describe Propane do
before(:all) do
@statics = {}
end
def write_grammar(grammar, options = {})
options[:name] ||= ""
File.write("spec/run/testparser#{options[:name]}.propane", grammar)
end
def run_propane(options = {})
options[:language] ||= "d"
@statics[:build_test_id] ||= 0
@statics[:build_test_id] += 1
if ENV["dist_specs"]
command = %W[dist/propane]
else
command = %W[ruby -I spec/run -r _simplecov_setup -I lib bin/propane]
command_prefix =
if ENV["partial_specs"]
"p"
else
"b"
end
command_name = "#{command_prefix}#{@statics[:build_test_id]}"
File.open("spec/run/_simplecov_setup.rb", "w") do |fh|
fh.puts <<EOF
require "bundler"
Bundler.setup
require "simplecov"
class MyFormatter
def format(*args)
end
end
SimpleCov.start do
command_name(#{command_name.inspect})
filters.clear
add_filter do |src|
!(src.filename[SimpleCov.root])
end
formatter(MyFormatter)
end
# force color off
ENV["TERM"] = nil
EOF
end
end
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
command += (options[:extra_args] || [])
if (options[:capture])
stdout, stderr, status = Open3.capture3(*command)
Results.new(stdout, stderr, status)
else
result = system(*command)
expect(result).to be_truthy
end
end
def compile(test_files, options = {})
test_files = Array(test_files).map do |test_file|
if !File.exist?(test_file) && test_file.end_with?(".cpp")
test_file.sub(%r{\.cpp$}, ".c")
else
test_file
end
end
options[:parsers] ||= [""]
parsers = options[:parsers].map do |name|
"spec/run/testparser#{name}.#{options[:language]}"
end
case options[:language]
when "c"
command = [*%w[gcc -g -Wall -o spec/run/testparser -Ispec -Ispec/run], *parsers, *test_files, "spec/testutils.c", "-lm"]
when "cpp"
command = [*%w[g++ -g -x c++ -Wall -o spec/run/testparser -Ispec -Ispec/run], *parsers, *test_files, "spec/testutils.c", "-lm"]
when "d"
command = [*%w[ldc2 -g --unittest -of spec/run/testparser -Ispec], *parsers, *test_files, "spec/testutils.d"]
end
result = system(*command)
expect(result).to be_truthy
end
def run_test(options)
stdout, stderr, status = Open3.capture3("spec/run/testparser")
File.binwrite("spec/run/.stderr", stderr)
File.binwrite("spec/run/.stdout", stdout)
stderr.sub!(/^.*modules passed unittests\n/, "")
results = Results.new(stdout, stderr, status)
# Valgrind is only reliably available on Linux, so limit the leak check to
# Linux platforms.
if RUBY_PLATFORM =~ /linux/
stdout, stderr, status = Open3.capture3("valgrind --leak-check=full --show-leak-kinds=all --track-origins=yes --verbose spec/run/testparser")
vgout = stdout + stderr
File.binwrite("spec/run/.vgout", vgout)
vgout.scan(/(?:definitely|indirectly) lost: (\d+) bytes/) do |match|
bytes = $1.to_i
if bytes > 0
raise "Valgrind detected memory leak"
end
end
end
results
end
def lines(str)
str.lines.map(&:chomp)
end
def verify_lines(lines, patterns)
if lines.is_a?(String)
lines = lines.lines.map(&:chomp)
end
patterns.each_with_index do |pattern, i|
found_index =
if pattern.is_a?(Regexp)
lines.find_index {|line| line =~ pattern}
else
lines.find_index do |line|
line.chomp == pattern.chomp
end
end
unless found_index
$stderr.puts "Lines:"
$stderr.puts lines
raise "A line matching #{pattern.inspect} (index #{i}) was not found."
end
end
end
before(:each) do
FileUtils.rm_rf("spec/run")
FileUtils.mkdir_p("spec/run")
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
it "raises an error when a pattern referenced ptype has not been defined" do
write_grammar <<EOF
ptype yes = int;
/foo/ (yes) <<
>>
/bar/ (no) <<
>>
EOF
results = run_propane(capture: true)
expect(results.stderr).to match /Error: Line 4: ptype no not declared\. Declare with `ptype` statement\./
expect(results.status).to_not eq 0
end
it "raises an error when a token referenced ptype has not been defined" do
write_grammar <<EOF
ptype yes = int;
token foo (yes);
token bar (no);
EOF
results = run_propane(capture: true)
expect(results.stderr).to match /Error: Line 3: ptype no not declared\. Declare with `ptype` statement\./
expect(results.status).to_not eq 0
end
it "raises an error when a rule referenced ptype has not been defined" do
write_grammar <<EOF
ptype yes = int;
token xyz;
foo (yes) -> bar;
bar (no) -> xyz;
EOF
results = run_propane(capture: true)
expect(results.stderr).to match /Error: Line 4: ptype no not declared\. Declare with `ptype` statement\./
expect(results.status).to_not eq 0
end
it "warns on shift/reduce conflicts" do
write_grammar <<EOF
token a;
token b;
Start -> As? b?;
As -> a As2?;
As2 -> b a As2?;
EOF
results = run_propane(capture: true)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
expect(File.binread("spec/run/testparser.log")).to match %r{Shift/Reduce conflict \(state \d+\) between token b and rule As2\? \(defined on line 4\)}
end
it "errors on shift/reduce conflicts with -w" do
write_grammar <<EOF
token a;
token b;
Start -> As? b?;
As -> a As2?;
As2 -> b a As2?;
EOF
results = run_propane(extra_args: %w[-w], capture: true)
expect(results.stderr).to match %r{Shift/Reduce conflict \(state \d+\) between token b and rule As2\? \(defined on line 4\)}m
expect(results.status).to_not eq 0
expect(File.binread("spec/run/testparser.log")).to match %r{Shift/Reduce conflict \(state \d+\) between token b and rule As2\? \(defined on line 4\)}
end
it "errors on duplicate field aliases in a rule" do
write_grammar <<EOF
token a;
token b;
Start -> a:foo b:foo;
EOF
results = run_propane(extra_args: %w[-w], capture: true)
expect(results.stderr).to match %r{Error: duplicate field alias `foo` for rule Start defined on line 3}
expect(results.status).to_not eq 0
end
it "errors when an alias is in different positions for different rules in a rule set when tree mode is enabled" do
write_grammar <<EOF
tree;
token a;
token b;
Start -> a:foo b;
Start -> b b:foo;
EOF
results = run_propane(extra_args: %w[-w], capture: true)
expect(results.stderr).to match %r{Error: conflicting tree node field positions for alias `foo`}
expect(results.status).to_not eq 0
end
it "does not error when an alias is in different positions for different rules in a rule set when tree mode is not enabled" do
write_grammar <<EOF
token a;
token b;
Start -> a:foo b;
Start -> b b:foo;
EOF
results = run_propane(extra_args: %w[-w], capture: true)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
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 cpp].each do |language|
context "#{language.upcase} language" do
it "generates a lexer" do
write_grammar <<EOF
token int /\\d+/;
token plus /\\+/;
token times /\\*/;
drop /\\s+/;
Start -> Foo;
Foo -> int <<>>
Foo -> plus <<>>
EOF
run_propane(language: language)
compile("spec/test_lexer.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "detects a lexer error when an unknown character is seen" do
case language
when "c", "cpp"
write_grammar <<EOF
ptype int;
token int /\\d+/ <<
int v = 0;
for (size_t i = 0u; i < match_length; i++)
{
v *= 10;
v += (match[i] - '0');
}
$$ = v;
>>
Start -> int << $$ = $1; >>
EOF
when "d"
write_grammar <<EOF
ptype int;
token int /\\d+/ <<
int v;
foreach (c; match)
{
v *= 10;
v += (c - '0');
}
$$ = v;
>>
Start -> int << $$ = $1; >>
EOF
end
run_propane(language: language)
compile("spec/test_lexer_unknown_character.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "generates a parser" do
write_grammar <<EOF
token plus /\\+/;
token times /\\*/;
token zero /0/;
token one /1/;
Start -> E;
E -> E times B;
E -> E plus B;
E -> B;
B -> zero;
B -> one;
EOF
run_propane(language: language)
end
it "generates a parser that does basic math - user guide example" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <math.h>
>>
ptype size_t;
token plus /\\+/;
token times /\\*/;
token power /\\*\\*/;
token integer /\\d+/ <<
size_t v = 0u;
for (size_t i = 0u; i < match_length; i++)
{
v *= 10;
v += (match[i] - '0');
}
$$ = v;
>>
token lparen /\\(/;
token rparen /\\)/;
drop /\\s+/;
Start -> E1 << $$ = $1; >>
E1 -> E2 << $$ = $1; >>
E1 -> E1 plus E2 << $$ = $1 + $3; >>
E2 -> E3 << $$ = $1; >>
E2 -> E2 times E3 << $$ = $1 * $3; >>
E3 -> E4 << $$ = $1; >>
E3 -> E3 power E4 << $$ = (size_t)pow($1, $3); >>
E4 -> integer << $$ = $1; >>
E4 -> lparen E1 rparen << $$ = $2; >>
EOF
when "d"
write_grammar <<EOF
<<
import std.math;
>>
ptype ulong;
token plus /\\+/;
token times /\\*/;
token power /\\*\\*/;
token integer /\\d+/ <<
ulong v;
foreach (c; match)
{
v *= 10;
v += (c - '0');
}
$$ = v;
>>
token lparen /\\(/;
token rparen /\\)/;
drop /\\s+/;
Start -> E1 << $$ = $1; >>
E1 -> E2 << $$ = $1; >>
E1 -> E1 plus E2 << $$ = $1 + $3; >>
E2 -> E3 << $$ = $1; >>
E2 -> E2 times E3 << $$ = $1 * $3; >>
E3 -> E4 << $$ = $1; >>
E3 -> E3 power E4 << $$ = pow($1, $3); >>
E4 -> integer << $$ = $1; >>
E4 -> lparen E1 rparen << $$ = $2; >>
EOF
end
run_propane(language: language)
compile("spec/test_basic_math_grammar.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "generates an SLR parser" do
write_grammar <<EOF
token one /1/;
Start -> E;
E -> one E;
E -> one;
EOF
run_propane(language: language)
end
it "distinguishes between multiple identical rules with lookahead symbol" do
write_grammar <<EOF
token a;
token b;
Start -> R1 a;
Start -> R2 b;
R1 -> a b;
R2 -> a b;
EOF
run_propane(language: language)
compile("spec/test_parser_identical_rules_lookahead.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
end
it "handles reducing a rule that could be arrived at from multiple states" do
write_grammar <<EOF
token a;
token b;
drop /\\s+/;
Start -> a R1;
Start -> b R1;
R1 -> b;
EOF
run_propane(language: language)
compile("spec/test_parser_rule_from_multiple_states.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
end
it "executes user code when matching lexer token" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
>>
token abc <<
printf("abc!\\n");
>>
token def;
Start -> Abcs def;
Abcs -> ;
Abcs -> abc Abcs;
EOF
when "d"
write_grammar <<EOF
<<
import std.stdio;
>>
token abc <<
writeln("abc!");
>>
token def;
Start -> Abcs def;
Abcs -> ;
Abcs -> abc Abcs;
EOF
end
run_propane(language: language)
compile("spec/test_user_code.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
verify_lines(results.stdout, [
"abc!",
"pass1",
"abc!",
"abc!",
"pass2",
])
end
it "supports a pattern statement" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
>>
token abc;
/def/ <<
printf("def!\\n");
>>
Start -> abc;
EOF
when "d"
write_grammar <<EOF
<<
import std.stdio;
>>
token abc;
/def/ << writeln("def!"); >>
Start -> abc;
EOF
end
run_propane(language: language)
compile("spec/test_pattern.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
verify_lines(results.stdout, [
"def!",
"pass1",
"def!",
"def!",
"pass2",
])
end
it "supports returning tokens from pattern code blocks" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
>>
token abc;
/def/ << printf("def!\\n"); >>
/ghi/ <<
printf("ghi!\\n");
return $token(abc);
>>
Start -> abc;
EOF
when "d"
write_grammar <<EOF
<<
import std.stdio;
>>
token abc;
/def/ << writeln("def!"); >>
/ghi/ <<
writeln("ghi!");
return $token(abc);
>>
Start -> abc;
EOF
end
run_propane(language: language)
compile("spec/test_return_token_from_pattern.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
verify_lines(results.stdout, [
"def!",
"ghi!",
"def!",
])
end
it "supports lexer modes" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
>>
token abc;
token def;
tokenid string;
drop /\\s+/;
/"/ <<
printf("begin string mode\\n");
$mode(string);
>>
string: /[^"]+/ <<
printf("captured string\\n");
>>
string: /"/ <<
$mode(default);
return $token(string);
>>
Start -> abc string def;
EOF
when "d"
write_grammar <<EOF
<<
import std.stdio;
>>
token abc;
token def;
tokenid string;
drop /\\s+/;
/"/ <<
writeln("begin string mode");
$mode(string);
>>
string: /[^"]+/ <<
writeln("captured string");
>>
string: /"/ <<
$mode(default);
return $token(string);
>>
Start -> abc string def;
EOF
end
run_propane(language: language)
compile("spec/test_lexer_modes.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
verify_lines(results.stdout, [
"begin string mode",
"captured string",
"pass1",
"begin string mode",
"captured string",
"pass2",
])
end
it "multiple lexer modes may apply to a pattern" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
>>
ptype char;
token abc;
token def;
default, identonly: token ident /[a-z]+/ <<
$$ = match[0];
$mode(default);
return $token(ident);
>>
token dot /\\./ <<
$mode(identonly);
>>
default, identonly: drop /\\s+/;
Start -> abc dot ident <<
printf("ident: %c\\n", $3);
>>
EOF
when "d"
write_grammar <<EOF
<<
import std.stdio;
>>
ptype char;
token abc;
token def;
default, identonly: token ident /[a-z]+/ <<
$$ = match[0];
$mode(default);
>>
token dot /\\./ <<
$mode(identonly);
>>
default, identonly: drop /\\s+/;
Start -> abc dot ident <<
writeln("ident: ", $3);
>>
EOF
end
run_propane(language: language)
compile("spec/test_lexer_multiple_modes.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
verify_lines(results.stdout, [
"ident: d",
"pass1",
"ident: a",
"pass2",
])
end
it "executes user code associated with a parser rule" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
>>
token a;
token b;
Start -> A B << printf("Start!\\n"); >>
A -> a << printf("A!\\n"); >>
B -> b << printf("B!\\n"); >>
EOF
when "d"
write_grammar <<EOF
<<
import std.stdio;
>>
token a;
token b;
Start -> A B << writeln("Start!"); >>
A -> a << writeln("A!"); >>
B -> b << writeln("B!"); >>
EOF
end
run_propane(language: language)
compile("spec/test_parser_rule_user_code.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
verify_lines(results.stdout, [
"A!",
"B!",
"Start!",
])
end
it "executes user code associated with a parser rule in tree mode" do
case language
when "c"
write_grammar <<EOF
tree;
context_user_fields <<
int start_n_fields;
int start_a_value;
int a_value;
int b_value;
p_token_t b_token;
int c_is_null;
int c_field_is_null;
int alias_a_value;
int alias_b_value;
>>
ptype int;
token a << $$ = 11; >>
token b << $$ = 22; >>
Start -> A:ay B:bee C <<
${context.start_n_fields} = p_node_n_fields($$);
${context.start_a_value} = p_tree_walk_Start($$, pA, pToken1, pvalue);
${context.a_value} = p_tree_walk_A($1, pToken1, pvalue);
${context.b_value} = p_tree_walk_B($2, pToken1, pvalue);
${context.b_token} = p_tree_walk_B($2, pToken1, token);
${context.c_field_is_null} = p_node_valid(p_Start_pC($$)) ? 0 : 1;
${context.alias_a_value} = p_tree_walk_A(${ay}, pToken1, pvalue);
${context.alias_b_value} = p_tree_walk_B(${bee}, pToken1, pvalue);
>>
A -> a;
B -> b;
C -> << ${context.c_is_null} = p_node_valid($$) ? 0 : 1; >>
EOF
when "cpp"
write_grammar <<EOF
tree;
context_user_fields <<
int start_n_fields;
int start_a_value;
int a_value;
int b_value;
p_token_t b_token;
int c_is_null;
int c_field_is_null;
int alias_a_value;
int alias_b_value;
>>
ptype int;
token a << $$ = 11; >>
token b << $$ = 22; >>
Start -> A:ay B:bee C <<
${context.start_n_fields} = $$.n_fields();
${context.start_a_value} = $$.pA().pToken1().pvalue();
${context.a_value} = $1.pToken1().pvalue();
${context.b_value} = $2.pToken1().pvalue();
${context.b_token} = $2.pToken1().token();
${context.c_field_is_null} = $$.pC().valid() ? 0 : 1;
${context.alias_a_value} = ${ay}.pToken1().pvalue();
${context.alias_b_value} = ${bee}.pToken1().pvalue();
>>
A -> a;
B -> b;
C -> << ${context.c_is_null} = $$.valid() ? 0 : 1; >>
EOF
when "d"
write_grammar <<EOF
tree;
context_user_fields <<
int start_n_fields;
int start_a_value;
int a_value;
int b_value;
p_token_t b_token;
int c_is_null;
int c_field_is_null;
int alias_a_value;
int alias_b_value;
>>
ptype int;
token a << $$ = 11; >>
token b << $$ = 22; >>
Start -> A:ay B:bee C <<
${context.start_n_fields} = $$.n_fields;
${context.start_a_value} = $$.pA.pToken1.pvalue;
${context.a_value} = $1.pToken1.pvalue;
${context.b_value} = $2.pToken1.pvalue;
${context.b_token} = $2.pToken1.token;
${context.c_field_is_null} = ($$.pC.valid) ? 0 : 1;
${context.alias_a_value} = ${ay}.pToken1.pvalue;
${context.alias_b_value} = ${bee}.pToken1.pvalue;
>>
A -> a;
B -> b;
C -> << ${context.c_is_null} = ($$.valid) ? 0 : 1; >>
EOF
end
run_propane(language: language)
compile("spec/test_parser_user_code_tree.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "parses lists" do
write_grammar <<EOF
ptype #{language == "d" ? "uint" : "uint32_t"};
token a;
Start -> As << $$ = $1; >>
As -> << $$ = 0u; >>
As -> As a << $$ = $1 + 1u; >>
EOF
run_propane(language: language)
compile("spec/test_parsing_lists.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
expect(results.stderr).to eq ""
end
it "fails to generate a parser for a LR(1) grammar that is not LALR" do
write_grammar <<EOF
token a;
token b;
token c;
token d;
token e;
Start -> a E c;
Start -> a F d;
Start -> b F c;
Start -> b E d;
E -> e;
F -> e;
EOF
results = run_propane(capture: true, language: language)
expect(results.status).to_not eq 0
expect(results.stderr).to match %r{Error: reduce/reduce conflict \(state \d+\) between rule E#\d+ \(defined on line 10\) and rule F#\d+ \(defined on line 11\)}
expect(File.binread("spec/run/testparser.log")).to match %r{Reduce.actions:}
end
it "provides matched text to user code blocks" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
#include <stdlib.h>
>>
token id /[a-zA-Z_][a-zA-Z0-9_]*/ <<
char * t = (char *)malloc(match_length + 1);
strncpy(t, (char *)match, match_length);
printf("Matched token is %s\\n", t);
free(t);
>>
Start -> id;
EOF
when "d"
write_grammar <<EOF
<<
import std.stdio;
>>
token id /[a-zA-Z_][a-zA-Z0-9_]*/ <<
writeln("Matched token is ", match);
>>
Start -> id;
EOF
end
run_propane(language: language)
compile("spec/test_lexer_match_text.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
verify_lines(results.stdout, [
"Matched token is identifier_123",
"pass1",
])
end
it "allows storing a result value for the lexer" do
case language
when "c", "cpp"
write_grammar <<EOF
ptype uint64_t;
token word /[a-z]+/ <<
$$ = match_length;
>>
Start -> word <<
$$ = $1;
>>
EOF
when "d"
write_grammar <<EOF
ptype ulong;
token word /[a-z]+/ <<
$$ = match.length;
>>
Start -> word <<
$$ = $1;
>>
EOF
end
run_propane(language: language)
compile("spec/test_lexer_result_value.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "tracks position of parser errors" do
write_grammar <<EOF
token a;
token num /\\d+/;
drop /\\s+/;
Start -> a num Start;
Start -> a num;
EOF
run_propane(language: language)
compile("spec/test_error_positions.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows setting the text position via p_set_position()" do
write_grammar <<EOF
token a;
token b;
Start -> a b;
EOF
run_propane(language: language)
compile("spec/test_set_position.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "exposes the current input byte offset via p_input_index()" do
write_grammar <<EOF
drop /\\s+/;
token a;
token b;
start Start;
Start -> a b;
EOF
run_propane(language: language)
compile("spec/test_input_index.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows rewinding the input to repeat a section during a parse" do
ext = language == "cpp" ? "c" : language
write_grammar(File.read("spec/rewind.#{ext}.propane"))
run_propane(language: language)
compile("spec/test_rewind.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows creating a JSON parser" do
ext = language == "cpp" ? "c" : language
write_grammar(File.read("spec/json_parser.#{ext}.propane"))
run_propane(language: language)
compile(["spec/test_parsing_json.#{language}", "spec/json_types.#{language}"], language: language)
end
it "allows generating multiple parsers in the same program" do
write_grammar(<<EOF, name: "myp1")
prefix myp1_;
token a;
token num /\\d+/;
drop /\\s+/;
Start -> a num;
EOF
run_propane(name: "myp1", language: language)
write_grammar(<<EOF, name: "myp2")
prefix myp2_;
token b;
token c;
Start -> b c b;
EOF
run_propane(name: "myp2", language: language)
compile("spec/test_multiple_parsers.#{language}", parsers: %w[myp1 myp2], language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows the user to terminate the lexer" do
write_grammar <<EOF
token a;
token b <<
$terminate(8675309);
>>
token c;
Start -> Any;
Any -> a;
Any -> b;
Any -> c;
EOF
run_propane(language: language)
compile("spec/test_user_terminate_lexer.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows lexer code blocks to access the matched token positions" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
>>
context_user_fields <<
p_position_t last_start;
p_position_t last_end;
>>
drop /\\s+/;
token word /[a-z]+/ <<
${context.last_start} = ${position};
${context.last_end} = ${end_position};
>>
token stop /!/ <<
$terminate(42);
>>
Start -> Words;
Words -> ;
Words -> word Words;
Words -> stop Words;
EOF
when "d"
write_grammar <<EOF
context_user_fields <<
p_position_t last_start;
p_position_t last_end;
>>
drop /\\s+/;
token word /[a-z]+/ <<
${context.last_start} = ${position};
${context.last_end} = ${end_position};
>>
token stop /!/ <<
$terminate(42);
>>
Start -> Words;
Words -> ;
Words -> word Words;
Words -> stop Words;
EOF
end
run_propane(language: language)
compile("spec/test_lexer_positions.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows the user to terminate the parser" do
write_grammar <<EOF
token a;
token b;
token c;
Start -> Any;
Any -> a Any;
Any -> b Any << $terminate(4200); >>
Any -> c Any;
Any -> ;
EOF
run_propane(language: language)
compile("spec/test_user_terminate.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "matches backslash escape sequences" do
case language
when "c", "cpp"
write_grammar <<EOF
<<
#include <stdio.h>
>>
tokenid t;
/\\a/ << printf("A\\n"); >>
/\\b/ << printf("B\\n"); >>
/\\t/ << printf("T\\n"); >>
/\\n/ << printf("N\\n"); >>
/\\v/ << printf("V\\n"); >>
/\\f/ << printf("F\\n"); >>
/\\r/ << printf("R\\n"); >>
/t/ << return $token(t); >>
Start -> t;
EOF
when "d"
write_grammar <<EOF
<<
import std.stdio;
>>
tokenid t;
/\\a/ << writeln("A"); >>
/\\b/ << writeln("B"); >>
/\\t/ << writeln("T"); >>
/\\n/ << writeln("N"); >>
/\\v/ << writeln("V"); >>
/\\f/ << writeln("F"); >>
/\\r/ << writeln("R"); >>
/t/ <<
return $token(t);
>>
Start -> t;
EOF
end
run_propane(language: language)
compile("spec/test_match_backslashes.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
verify_lines(results.stdout, [
"A",
"B",
"T",
"N",
"V",
"F",
"R",
])
end
it "handles when an item set leads to itself" do
write_grammar <<EOF
token one;
token two;
Start -> Opt one Start;
Start -> ;
Opt -> two;
Opt -> ;
EOF
run_propane(language: language)
end
it "generates a tree" do
write_grammar <<EOF
tree;
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_tree.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "supports tree node prefix and suffix" do
write_grammar <<EOF
tree;
tree_prefix P ;
tree_suffix S;
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_tree_ps.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows specifying a different start rule" do
write_grammar <<EOF
token hi;
start Top;
Top -> hi;
EOF
run_propane(language: language)
compile("spec/test_start_rule.#{language}", language: language)
end
it "allows specifying a different start rule with tree generation" do
write_grammar <<EOF
tree;
token hi;
start Top;
Top -> hi;
EOF
run_propane(language: language)
compile("spec/test_start_rule_tree.#{language}", language: language)
end
it "allows marking a rule component as optional" do
if language == "d"
write_grammar <<EOF
<<
import std.stdio;
>>
ptype int;
ptype float = float;
ptype string = string;
token a (float) << $$ = 1.5; >>
token b << $$ = 2; >>
token c << $$ = 3; >>
token d << $$ = 4; >>
Start -> a? b R? <<
writeln("a: ", $1);
writeln("b: ", $2);
writeln("R: ", $3);
>>
R -> c d << $$ = "cd"; >>
R (string) -> d c << $$ = "dc"; >>
EOF
else
write_grammar <<EOF
<<
#include <stdio.h>
>>
ptype int;
ptype float = float;
ptype string = char *;
token a (float) << $$ = 1.5; >>
token b << $$ = 2; >>
token c << $$ = 3; >>
token d << $$ = 4; >>
Start -> a? b R? <<
printf("a: %.1f\\n", $1);
printf("b: %d\\n", $2);
printf("R: %s\\n", $3 == NULL ? "" : $3);
>>
R -> c d << $$ = (char *)"cd"; >>
R (string) -> d c << $$ = (char *)"dc"; >>
EOF
end
run_propane(language: language)
compile("spec/test_optional_rule_component.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
verify_lines(results.stdout, [
"a: 0#{language == "d" ? "" : ".0"}",
"b: 2",
"R: ",
"a: 1.5",
"b: 2",
"R: cd",
"a: 1.5",
"b: 2",
"R: dc",
])
end
it "allows marking a rule component as optional in tree generation mode" do
if language == "d"
write_grammar <<EOF
tree;
<<
import std.stdio;
>>
token a;
token b;
token c;
token d;
Start -> a? b R?;
R -> c d;
R -> d c;
EOF
else
write_grammar <<EOF
tree;
<<
#include <stdio.h>
>>
token a;
token b;
token c;
token d;
Start -> a? b R?;
R -> c d;
R -> d c;
EOF
end
run_propane(language: language)
compile("spec/test_optional_rule_component_tree.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows naming an optional rule component in tree generation mode" do
if language == "d"
write_grammar <<EOF
tree;
<<
import std.stdio;
>>
token a;
token b;
token c;
token d;
Start -> a?:a b R?:r;
R -> c d;
R -> d c;
EOF
else
write_grammar <<EOF
tree;
<<
#include <stdio.h>
>>
token a;
token b;
token c;
token d;
Start -> a?:a b R?:r;
R -> c d;
R -> d c;
EOF
end
run_propane(language: language)
compile("spec/test_named_optional_rule_component_tree.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "stores token and rule positions in tree nodes" do
write_grammar <<EOF
tree;
token a;
token bb;
token c /c(.|\\n)*c/;
drop /\\s+/;
Start -> T T T;
T -> a;
T -> bb;
T -> c;
EOF
run_propane(language: language)
compile("spec/test_tree_token_positions.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "stores invalid positions for empty rule matches" do
write_grammar <<EOF
tree;
token a;
token bb;
token c /c(.|\\n)*c/;
drop /\\s+/;
Start -> T Start;
Start -> ;
T -> a A;
A -> bb? c?;
EOF
run_propane(language: language)
compile("spec/test_tree_invalid_positions.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows specifying field aliases in tree mode" do
write_grammar <<EOF
tree;
token a;
token b;
token c;
drop /\\s+/;
Start -> T:first T:second T:third;
T -> a;
T -> b;
T -> c;
EOF
run_propane(language: language)
compile("spec/test_tree_field_aliases.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "aliases the correct field when multiple rules are in a rule set in tree mode" do
write_grammar <<EOF
tree;
token a;
token b;
token c;
drop /\\s+/;
Start -> a;
Start -> Foo;
Start -> T:first T:second T:third;
Foo -> b;
T -> a;
T -> b;
T -> c;
EOF
run_propane(language: language)
compile("spec/test_tree_field_aliases.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows specifying field aliases when tree mode is not enabled" do
if language == "d"
write_grammar <<EOF
<<
import std.stdio;
>>
ptype string;
token id /[a-zA-Z_][a-zA-Z0-9_]*/ <<
$$ = match;
>>
drop /\\s+/;
Start -> id:first id:second <<
writeln("first is ", ${first});
writeln("second is ", ${second});
>>
EOF
else
write_grammar <<EOF
<<
#include <stdio.h>
#include <string.h>
>>
ptype char *;
token id /[a-zA-Z_][a-zA-Z0-9_]*/ <<
char * s = (char *)malloc(match_length + 1);
strncpy(s, (char const *)match, match_length);
s[match_length] = 0;
$$ = s;
>>
drop /\\s+/;
Start -> id:first id:second <<
printf("first is %s\\n", ${first});
printf("second is %s\\n", ${second});
free(${first});
free(${second});
>>
EOF
end
run_propane(language: language)
compile("spec/test_field_aliases.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
expect(results.stdout).to match /first is foo1.*second is bar2/m
end
it "aliases the correct field when multiple rules are in a rule set when tree mode is not enabled" do
if language == "d"
write_grammar <<EOF
<<
import std.stdio;
>>
ptype string;
token id /[a-zA-Z_][a-zA-Z0-9_]*/ <<
$$ = match;
>>
drop /\\s+/;
Start -> id;
Start -> Foo;
Start -> id:first id:second <<
writeln("first is ", ${first});
writeln("second is ", ${second});
>>
Foo -> ;
EOF
else
write_grammar <<EOF
<<
#include <stdio.h>
#include <string.h>
>>
ptype char *;
token id /[a-zA-Z_][a-zA-Z0-9_]*/ <<
char * s = (char *)malloc(match_length + 1);
strncpy(s, (char const *)match, match_length);
s[match_length] = 0;
$$ = s;
>>
drop /\\s+/;
Start -> id;
Start -> Foo;
Start -> id:first id:second <<
printf("first is %s\\n", ${first});
printf("second is %s\\n", ${second});
free(${first});
free(${second});
>>
Foo -> ;
EOF
end
run_propane(language: language)
compile("spec/test_field_aliases.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
expect(results.stdout).to match /first is foo1.*second is bar2/m
end
# D garbage collector was freeing memory for nodes when it should not
# have been. This was due to pointers being in the TreeNode that was
# allocated by C malloc() function instead of allocated using D.
it "does not free memory allocated for tree nodes" do
ext = language == "cpp" ? "c" : language
write_grammar(File.read("spec/tree_node_memory_remains.#{ext}.propane"))
run_propane(language: language)
compile("spec/test_tree_node_memory_remains.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows multiple starting rules" do
write_grammar <<EOF
ptype int;
token a << $$ = 1; >>
token b << $$ = 2; >>
token c << $$ = 3; >>
Start -> a b R;
Start -> Bs:bs << $$ = $1; >>
R -> c:c << $$ = $1; >>
Bs -> << $$ = 0; >>
Bs -> b:b Bs:bs << $$ = $1 + $2; >>
start Start R Bs;
EOF
run_propane(language: language)
compile("spec/test_starting_rules.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "supports parse_inner APIs that treat provided tokens as follow tokens" do
write_grammar <<EOF
ptype int;
token a << $$ = 1; >>
token b << $$ = 2; >>
Start -> Y << $$ = $1; >>
Y -> a << $$ = $1; >>
EOF
run_propane(language: language)
compile("spec/test_parse_inner.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "parse_inner APIs block success when the outer rule is unfinished" do
write_grammar <<EOF
ptype int;
token a << $$ = 1; >>
token b << $$ = 2; >>
token c << $$ = 3; >>
Start -> a Start b << $$ = $2; >>
Start -> c << $$ = $1; >>
EOF
run_propane(language: language)
compile("spec/test_parse_inner_recursive.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "parse_inner APIs work when the reduce state uses lookahead disambiguation" do
write_grammar <<EOF
ptype int;
token a;
token b;
start Start;
start R1;
Start -> R1 a;
Start -> R2 b;
R1 -> a b << $$ = 11; >>
R2 -> a b << $$ = 22; >>
EOF
run_propane(language: language)
compile("spec/test_parse_inner_shared.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "parse_inner APIs work in tree generation mode with follow tokens" do
write_grammar <<EOF
tree;
token a;
token b;
start Start;
start R1;
Start -> R1 a;
Start -> R2 b;
R1 -> a b;
R2 -> a b;
EOF
run_propane(language: language)
compile("spec/test_parse_inner_tree.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "supports a reentrant nested parse driven from a custom lex function" do
ext = language == "cpp" ? "c" : language
write_grammar(File.read("spec/parse_inner_nested.#{ext}.propane"))
run_propane(language: language)
compile("spec/test_parse_inner_nested.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "tracks positions across a lex-function nested parse in tree mode" do
ext = language == "cpp" ? "c" : language
write_grammar(File.read("spec/parse_inner_nested_tree.#{ext}.propane"))
run_propane(language: language)
compile("spec/test_parse_inner_nested_tree.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows multiple starting rules in tree mode" do
write_grammar <<EOF
tree;
ptype int;
token a << $$ = 1; >>
token b << $$ = 2; >>
token c << $$ = 3; >>
Start -> a b R;
Start -> Bs:bs;
R -> c:c;
Bs -> ;
Bs -> b:b Bs:bs;
start Start R Bs;
EOF
run_propane(language: language)
compile("spec/test_starting_rules_tree.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
if %w[c cpp].include?(language)
it "allows a user function to free token node memory in tree mode" do
write_grammar <<EOF
tree;
free_token_node <<
free(${token.pvalue});
>>
ptype int *;
token a <<
$$ = (int *)malloc(sizeof(int));
*$$ = 1;
>>
token b <<
$$ = (int *)malloc(sizeof(int));
*$$ = 2;
>>
Start -> a:a b:b;
EOF
run_propane(language: language)
compile("spec/test_tree_delete_token_node_memory.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
end
it "executes code blocks associated with drop statements" do
if language == "d"
write_grammar <<EOF
<<
import std.stdio;
>>
drop /\\s+/;
drop /#(.*)\\n/ <<
stderr.write("comment: ", match);
>>
token a;
Start -> a;
EOF
else
write_grammar <<EOF
<<
#include <stdio.h>
#include <string.h>
>>
drop /\\s+/;
drop /#(.*)\\n/ <<
fprintf(stderr, "comment: %.*s", (int)match_length, match);
>>
token a;
Start -> a;
EOF
end
run_propane(language: language)
compile("spec/test_drop_code_block.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to match %r{comment: # comment 1\n.*comment: # comment 2}m
expect(results.status).to eq 0
end
it "allows user-defined context fields" do
if language == "d"
write_grammar <<EOF
context_user_fields <<
string comments;
uint acount;
>>
drop /\\s+/;
drop /#(.*)\\n/ <<
${context.comments} ~= match;
>>
token a <<
${context.acount}++;
>>
Start -> As;
As -> ;
As -> a As;
EOF
else
write_grammar <<EOF
<<
#include <string.h>
#include <stdlib.h>
>>
context_user_fields <<
char * comments;
unsigned int acount;
>>
drop /\\s+/;
drop /#(.*)\\n/ <<
size_t cur_len = 0u;
if (${context.comments} != NULL)
cur_len = strlen(${context.comments});
char * commentsnew = (char *)malloc(cur_len + match_length + 1);
if (${context.comments} != NULL)
memcpy(commentsnew, ${context.comments}, cur_len);
memcpy(&commentsnew[cur_len], match, match_length);
commentsnew[cur_len + match_length] = '\\0';
if (${context.comments} != NULL)
{
free(${context.comments});
}
${context.comments} = commentsnew;
>>
token a <<
${context.acount}++;
>>
Start -> As;
As -> ;
As -> a As;
EOF
end
run_propane(language: language)
compile("spec/test_user_context_fields.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to include %r{comments: # comment 1\n# comment 2}
expect(results.stderr).to include %r{acount: 11\n}
expect(results.status).to eq 0
end
it "allows custom token user fields" do
if language == "d"
write_grammar <<EOF
context_user_fields <<
string comments;
>>
token_user_fields <<
string comments;
>>
on_token_node <<
${token.comments} = ${context.comments};
${context.comments} = "";
>>
tree;
drop /\\s+/;
drop /#(.*)\\n/ <<
${context.comments} ~= match;
>>
token id /\\w+/;
Start -> IDs;
IDs -> ;
IDs -> id:id IDs;
EOF
elsif language == "c"
write_grammar <<EOF
<<
#include <string.h>
#include <stdlib.h>
>>
context_user_fields <<
char * comments;
>>
token_user_fields <<
char * comments;
>>
free_token_node <<
free(${token.comments});
>>
on_token_node <<
${token.comments} = ${context.comments};
${context.comments} = (char *)malloc(1);
${context.comments}[0] = '\\0';
>>
tree;
drop /\\s+/;
drop /#(.*)\\n/ <<
size_t cur_len = 0u;
if (${context.comments} != NULL)
cur_len = strlen(${context.comments});
char * commentsnew = (char *)malloc(cur_len + match_length + 1);
if (${context.comments} != NULL)
memcpy(commentsnew, ${context.comments}, cur_len);
memcpy(&commentsnew[cur_len], match, match_length);
commentsnew[cur_len + match_length] = '\\0';
if (${context.comments} != NULL)
{
free(${context.comments});
}
${context.comments} = commentsnew;
>>
token id /\\w+/;
Start -> IDs;
IDs -> ;
IDs -> id:id IDs;
EOF
else # C++
write_grammar <<EOF
<<header
#include <string>
>>
context_user_fields <<
std::string comments;
>>
token_user_fields <<
std::string comments;
>>
on_token_node <<
${token.comments} = ${context.comments};
${context.comments} = "";
>>
tree;
drop /\\s+/;
drop /#(.*)\\n/ <<
${context.comments} += std::string((const char *)match, match_length);
>>
token id /\\w+/;
Start -> IDs;
IDs -> ;
IDs -> id:id IDs;
EOF
end
run_propane(language: language)
compile("spec/test_token_user_fields.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
end
it "allows a custom lex function" do
if language == "d"
write_grammar <<EOF
<<
private size_t mylexfn(p_context_t * context, p_token_info_t * out_token_info)
{
static size_t count;
size_t result = P_SUCCESS;
if (count > 0)
{
out_token_info.token = TOKEN_a;
out_token_info.pvalue = p_value(count);
count--;
}
else
{
result = p_lex(context, out_token_info);
if (out_token_info.token == TOKEN_c)
{
count = 3;
}
}
return result;
}
>>
ptype size_t;
lex_fn mylexfn;
token a << $$ = 7; >>
token b << $$ = 8; >>
token c << $$ = 9; >>
Start -> << $$ = 0; >>
Start -> Start ID << $$ = ($1 << 4) | $2; >>
ID -> a << $$ = $1; >>
ID -> b << $$ = $1; >>
ID -> c << $$ = $1; >>
EOF
else
write_grammar <<EOF
<<
static size_t mylexfn(p_context_t * context, p_token_info_t * out_token_info)
{
static size_t count;
size_t result = P_SUCCESS;
if (count > 0)
{
out_token_info->token = TOKEN_a;
out_token_info->pvalue = p_value(count);
count--;
}
else
{
result = p_lex(context, out_token_info);
if (out_token_info->token == TOKEN_c)
{
count = 3;
}
}
return result;
}
>>
ptype size_t;
lex_fn mylexfn;
token a << $$ = 7; >>
token b << $$ = 8; >>
token c << $$ = 9; >>
Start -> << $$ = 0; >>
Start -> Start ID << $$ = ($1 << 4) | $2; >>
ID -> a << $$ = $1; >>
ID -> b << $$ = $1; >>
ID -> c << $$ = $1; >>
EOF
end
run_propane(language: language)
compile("spec/test_custom_lex_fn.#{language}", language: language)
results = run_test(language: language)
expect(results.status).to eq 0
end
it "provides accessor functions to extract user values from a parser value" do
if language == "d"
write_grammar <<EOF
ptype int;
ptype float = float;
ptype string = string;
drop /\\s+/;
token num /\\d+/ << $$ = 42; >>
token flt (float) /f/ << $$ = 1.5; >>
token str (string) /s/ << $$ = "hello"; >>
Start -> num flt str;
EOF
else
write_grammar <<EOF
ptype int;
ptype float = float;
ptype string = char *;
drop /\\s+/;
token num /\\d+/ << $$ = 42; >>
token flt (float) /f/ << $$ = 1.5; >>
token str (string) /s/ << $$ = (char *)"hello"; >>
Start -> num flt str;
EOF
end
run_propane(language: language)
compile("spec/test_value_accessors.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows accessing rule and component text positions" do
if language == "d"
write_grammar <<EOF
<<
import std.stdio;
>>
drop /\\s+/;
token tok1;
token tok2;
token ident /[a-zA-Z_]\\w*/;
token num /\\d+/;
Num -> num;
Start -> ident Num <<
writeln("ident start: ", ${1.position}.row, ", ", ${1.position}.col);
writeln("ident end: ", ${1.end_position}.row, ", ", ${1.end_position}.col);
writeln("Num start: ", ${2.position}.row, ", ", ${2.position}.col);
writeln("Num end: ", ${2.end_position}.row, ", ", ${2.end_position}.col);
writeln("Start start: ", ${$.position}.row, ", ", ${$.position}.col);
writeln("Start end: ", ${$.end_position}.row, ", ", ${$.end_position}.col);
>>
R -> Empty tok2 <<
writeln("Empty start: ", ${1.position}.row, ", ", ${1.position}.col);
writeln("Empty end: ", ${1.end_position}.row, ", ", ${1.end_position}.col);
writeln("tok2 start: ", ${2.position}.row, ", ", ${2.position}.col);
writeln("tok2 end: ", ${2.end_position}.row, ", ", ${2.end_position}.col);
writeln("R start: ", ${$.position}.row, ", ", ${$.position}.col);
writeln("R end: ", ${$.end_position}.row, ", ", ${$.end_position}.col);
>>
R -> tok1 Empty <<
writeln("tok1 start: ", ${1.position}.row, ", ", ${1.position}.col);
writeln("tok1 end: ", ${1.end_position}.row, ", ", ${1.end_position}.col);
writeln("Empty start: ", ${2.position}.row, ", ", ${2.position}.col);
writeln("Empty end: ", ${2.end_position}.row, ", ", ${2.end_position}.col);
writeln("R2 start: ", ${$.position}.row, ", ", ${$.position}.col);
writeln("R2 end: ", ${$.end_position}.row, ", ", ${$.end_position}.col);
>>
Empty -> ;
Start -> R <<
writeln("StartR start: ", ${$.position}.row, ", ", ${$.position}.col);
writeln("StartR end: ", ${$.end_position}.row, ", ", ${$.end_position}.col);
>>
Start -> Empty <<
writeln("StartEmpty start: ", ${$.position}.row, ", ", ${$.position}.col);
writeln("StartEmpty end: ", ${$.end_position}.row, ", ", ${$.end_position}.col);
>>
EOF
else
write_grammar <<EOF
<<
#include <stdio.h>
>>
drop /\\s+/;
token tok1;
token tok2;
token ident /[a-zA-Z_]\\w*/;
token num /\\d+/;
Num -> num;
token pct /%/;
Start -> ident Num <<
printf("ident start: %d, %d\\n", ${1.position}.row, ${1.position}.col);
printf("ident end: %d, %d\\n", ${1.end_position}.row, ${1.end_position}.col);
printf("Num start: %d, %d\\n", ${2.position}.row, ${2.position}.col);
printf("Num end: %d, %d\\n", ${2.end_position}.row, ${2.end_position}.col);
printf("Start start: %d, %d\\n", ${$.position}.row, ${$.position}.col);
printf("Start end: %d, %d\\n", ${$.end_position}.row, ${$.end_position}.col);
>>
R -> Empty tok2 <<
printf("Empty start: %d, %d\\n", ${1.position}.row, ${1.position}.col);
printf("Empty end: %d, %d\\n", ${1.end_position}.row, ${1.end_position}.col);
printf("tok2 start: %d, %d\\n", ${2.position}.row, ${2.position}.col);
printf("tok2 end: %d, %d\\n", ${2.end_position}.row, ${2.end_position}.col);
printf("R start: %d, %d\\n", ${$.position}.row, ${$.position}.col);
printf("R end: %d, %d\\n", ${$.end_position}.row, ${$.end_position}.col);
>>
R -> tok1 Empty <<
printf("tok1 start: %d, %d\\n", ${1.position}.row, ${1.position}.col);
printf("tok1 end: %d, %d\\n", ${1.end_position}.row, ${1.end_position}.col);
printf("Empty start: %d, %d\\n", ${2.position}.row, ${2.position}.col);
printf("Empty end: %d, %d\\n", ${2.end_position}.row, ${2.end_position}.col);
printf("R2 start: %d, %d\\n", ${$.position}.row, ${$.position}.col);
printf("R2 end: %d, %d\\n", ${$.end_position}.row, ${$.end_position}.col);
>>
Empty -> ;
Start -> R <<
printf("StartR start: %d, %d\\n", ${$.position}.row, ${$.position}.col);
printf("StartR end: %d, %d\\n", ${$.end_position}.row, ${$.end_position}.col);
>>
Start -> Empty <<
printf("StartEmpty start: %d, %d\\n", ${$.position}.row, ${$.position}.col);
printf("StartEmpty end: %d, %d\\n", ${$.end_position}.row, ${$.end_position}.col);
>>
EOF
end
run_propane(language: language)
compile("spec/test_positions.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
expect(results.stdout).to eq <<EOF
ident start: 1, 5
ident end: 1, 9
Num start: 3, 9
Num end: 3, 12
Start start: 1, 5
Start end: 3, 12
Empty start: 0, 0
Empty end: 0, 0
tok2 start: 2, 2
tok2 end: 2, 5
R start: 2, 2
R end: 2, 5
StartR start: 2, 2
StartR end: 2, 5
tok1 start: 1, 3
tok1 end: 1, 6
Empty start: 0, 0
Empty end: 0, 0
R2 start: 1, 3
R2 end: 1, 6
StartR start: 1, 3
StartR end: 1, 6
EOF
end
it "allows nested parses for macro expansions" do
ext = language == "cpp" ? "c" : language
write_grammar(File.read("spec/macros.#{ext}.propane"))
run_propane(language: language)
compile("spec/test_macros.#{language}", language: language)
results = run_test(language: language)
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
end
end
end