14 Commits
Author SHA1 Message Date
josh cb914e8bb0 v4.5.0 2026-06-29 23:49:01 -04:00
josh ece95dd7e0 Fix propane.vim keyword detection 2026-06-29 23:48:43 -04:00
josh 232af68081 Attempt to autodetect target language (D/C++) in extra/vim/syntax/propane.vim 2026-06-29 23:02:57 -04:00
josh 23834bef82 Update keyword list in extra/vim/syntax/propane.vim 2026-06-29 23:02:05 -04:00
josh 7b698d7b31 Add noline grammar statement to skip emitting #line directives 2026-06-29 22:41:07 -04:00
josh 45843da0df Fix #line reset directives 2026-06-29 22:21:45 -04:00
josh f27a8688f7 Add CHANGELOG for v4.4.0 2026-06-29 22:15:01 -04:00
josh c459507612 v4.4.0 2026-06-22 23:46:32 -04:00
josh feacbc907d Update gems 2026-06-22 23:46:06 -04:00
josh 5ac3ad1655 Add p_value_get() / p_value_get_XXX() accessors
Allow user code to access specific pvalue values, especially when
multiple ptypes are in use.
2026-06-22 22:12:29 -04:00
josh b641c64425 v4.3.0 2026-05-22 18:16:29 -07:00
josh 5245302f49 Update CHANGELOG for v4.3.0 2026-05-22 18:11:58 -07:00
josh 7a140623ff Use #line for user code blocks to report input grammar position for errors 2026-05-21 16:24:25 -07:00
josh 42035c3a55 Update gems 2026-05-06 08:40:37 -04:00
15 changed files with 275 additions and 30 deletions

No files matched your search

+25
View File
@@ -1,3 +1,28 @@
## v4.5.0
### New Features
- Add `noline` grammar statement to skip emitting `#line` directives
- Attempt to autodetect target language (D/C++) in extra/vim/syntax/propane.vim
### Fixes
- Fix #line reset directives
- Update keyword list in extra/vim/syntax/propane.vim
- Fix propane.vim keyword detection
## v4.4.0
### New Features
- Add p_value_get() / p_value_get_XXX() accessors
## v4.3.0
### New Features
- Use #line for user code blocks to report input grammar position for errors.
## v4.2.0
### New Features
+4 -4
View File
@@ -5,11 +5,11 @@ GEM
date (3.5.1)
diff-lcs (1.6.2)
docile (1.4.1)
erb (6.0.2)
psych (5.3.1)
erb (6.0.4)
psych (5.4.0)
date
stringio
rake (13.3.1)
rake (13.4.2)
rdoc (7.2.0)
erb
psych (>= 4.0.0)
@@ -51,4 +51,4 @@ DEPENDENCIES
syntax
BUNDLED WITH
2.3.7
4.0.14
+8
View File
@@ -94,6 +94,14 @@ public <%= @grammar.prefix %>value_t <%= @grammar.prefix %>value<%= name == "def
return <%= @grammar.prefix %>value_t(v_<%= name %>: v);
}
<% end %>
/** Parser value accessor(s). */
<% @grammar.ptypes.each do |name, typestring| %>
public <%= typestring %> <%= @grammar.prefix %>value_get<%= name == "default" ? "" : "_#{name}" %>(<%= @grammar.prefix %>value_t * pvalue)
{
return pvalue.v_<%= name %>;
}
<% end %>
<% end %>
<% if @grammar.tree %>
+8
View File
@@ -77,6 +77,14 @@ static inline <%= @grammar.prefix %>value_t <%= @grammar.prefix %>value<%= name
return (<%= @grammar.prefix %>value_t){.v_<%= name %> = v};
}
<% end %>
/** Parser value accessor(s). */
<% @grammar.ptypes.each do |name, typestring| %>
static inline <%= typestring %> <%= @grammar.prefix %>value_get<%= name == "default" ? "" : "_#{name}" %>(<%= @grammar.prefix %>value_t const * pvalue)
{
return pvalue->v_<%= name %>;
}
<% end %>
<% end %>
<% if @grammar.tree %>
+46 -5
View File
@@ -383,6 +383,27 @@ module proj.parser;
If a module statement is not present, then the generated D module will not
contain a module statement and the default module name will be used.
##> `noline` statement - disabling `#line` directives
By default, Propane emits `#line` directives into the generated output around
user code blocks.
These directives instruct the compiler to report any warnings or errors in the
user code using the file name and line number of the original grammar file,
rather than the generated output file.
This makes it easier to locate the source of a compiler diagnostic in the
grammar file.
The `noline` statement disables the emission of these `#line` directives.
```
noline;
```
When the `noline` statement is present, user code blocks are copied to the
generated output without any surrounding `#line` directives.
This can be useful when debugging the generated parser itself, or when the
`#line` directives interfere with other tooling.
##> `on_tree_node` statement - custom initialization of a token tree node
The `on_token_node` statement can be used to provide code that initializes
@@ -1082,6 +1103,9 @@ The `p_token_info_t` structure contains the following fields:
* `length` (`size_t`) holds the number of input bytes used by the token.
* `token` (`p_token_t`) holds the token ID of the lexed token
* `pvalue` (`p_value_t`) holds the parser value associated with the token.
The actual user value can be extracted with `p_value_get(&token_info.pvalue)`
for the default value or `p_value_get_XXX(&token_info.pvalue)` for named
`ptype` values.
### Tree Node Types
@@ -1404,18 +1428,35 @@ In this case, Propane will free a `Statement` tree structure returned by the
### `p_value`
When tree generation mode is not active, the `p_value_t` union can hold one of
several different possible value types.
The `p_value(v)` function returns an instance of the `p_value_t` with the
The `p_value(v)` function builds an instance of the `p_value_t` with the
default member set to the value of `v`.
A `p_value_XXX(v)` function is set for each user-defined `ptype` name with
A `p_value_XXX(v)` function is defined for each user-defined `ptype` name with
the user-given name in place of the `XXX`.
These functions are useful for custom lexer functions which need to return a
parser value corresponding to a lexed token.
They are especially useful when tree generation mode is not active.
In that case, the `p_value_t` union can hold one of several different possible
value types.
### `p_value_get`
The `p_value_get()` accessor functions can be used to extract a user value
from a `p_value_t` union.
The `p_value_get(pvalue)` function accepts a pointer to a `p_value_t` and
returns the value of its default member.
A `p_value_get_XXX(pvalue)` function is generated for each user-defined `ptype`
name with the user-given name in place of the `XXX`, returning the value of the
corresponding member.
These functions are the counterpart to `p_value()` constructor functions and
are useful for reading the parser value associated with a lexed token, for
example `p_value_get(&token_info.pvalue)`.
##> Data
### `p_token_names`
+13 -2
View File
@@ -8,7 +8,11 @@ if exists("b:current_syntax")
endif
if !exists("b:propane_subtype")
let b:propane_subtype = "d"
if search('\<import\s\+\%(std\|core\)\.', 'nw') > 0
let b:propane_subtype = "d"
else
let b:propane_subtype = "cpp"
endif
endif
exe "syn include @propaneTarget syntax/".b:propane_subtype.".vim"
@@ -20,12 +24,19 @@ syn match propaneOperator "->"
syn match propaneFieldAlias ":[a-zA-Z0-9_]\+" contains=propaneFieldOperator
syn match propaneFieldOperator ":" contained
syn match propaneOperator "?"
syn keyword propaneKeyword drop free_token_node free_token_user_fields module prefix ptype start token token_user_fields tokenid tree tree_prefix tree_suffix
" Keywords that introduce a user-defined name. The name is consumed by
" propaneName via nextgroup so a name matching a keyword (e.g. 'token start')
" is not highlighted as a keyword. These must be a match (not syn keyword)
" because a syn keyword always wins over a contained nextgroup match.
syn match propaneNameDecl "\<\%(tokenid\|token\|lex_fn\|module\|start\|tree_prefix\|tree_suffix\)\>" nextgroup=propaneName skipwhite
syn match propaneName "\<\h\w*\>" contained
syn match propaneKeyword "\<\%(context_user_fields\|drop\|free_token_node\|noline\|on_token_node\|prefix\|ptype\|token_user_fields\|tree\)\>"
syn region propaneRegex start="/" end="/" skip="\v\\\\|\\/"
hi def link propaneComment Comment
hi def link propaneKeyword Keyword
hi def link propaneNameDecl Keyword
hi def link propaneRegex String
hi def link propaneOperator Operator
hi def link propaneFieldOperator Operator
+1 -1
View File
@@ -33,7 +33,7 @@ class Propane
def run(input_file, output_file, log_file, options)
begin
grammar = Grammar.new(File.read(input_file))
grammar = Grammar.new(File.read(input_file), input_file)
generator = Generator.new(grammar, output_file, log_file, options)
generator.generate
rescue Error => e
+7 -1
View File
@@ -38,7 +38,13 @@ class Propane
output_file = @output_file
end
erb = ERB.new(template, trim_mode: "<>")
result = erb.result(binding.clone)
result = erb.result(binding.clone).lines.each_with_index.map do |line, i|
if line == "#linereset\n"
%[#line #{i + 2} "#{output_file}"\n]
else
line
end
end.join
File.open(output_file, "wb") do |fh|
fh.write(result)
end
+26 -6
View File
@@ -22,7 +22,8 @@ class Propane
attr_reader :on_token_node
attr_reader :token_user_fields
def initialize(input)
def initialize(input, filename)
@filename = filename
@patterns = []
@start_rules = []
@tokens = []
@@ -87,6 +88,7 @@ class Propane
elsif parse_rule_statement!
elsif parse_code_block_statement!
elsif parse_prefix_statement!
elsif parse_noline_statement!
else
if @input.size > 25
@input = @input.slice(0..20) + "..."
@@ -304,8 +306,14 @@ class Propane
def parse_code_block_statement!
if md = consume!(/<<([a-z]*)(.*?)>>\n/m)
name, code = md[1..2]
code.sub!(/\A\n/, "")
code += "\n" unless code.end_with?("\n")
code = code.chomp
unless @noline
if code.start_with?("\n")
code = %[#line #{@line_number + 1} "#{@filename}"#{code}\n#linereset\n]
else
code = %[#line #{@line_number} "#{@filename}"\n#{code}\n#linereset\n]
end
end
if @code_blocks[name]
@code_blocks[name] += code
else
@@ -323,6 +331,13 @@ class Propane
end
end
def parse_noline_statement!
if md = consume!(/noline\s*;/)
@noline = true
true
end
end
def parse_pattern!
if md = consume!(%r{/})
pattern = ""
@@ -346,9 +361,14 @@ class Propane
def parse_code_block!
if md = consume!(/<<(.*?)>>\n/m)
code = md[1]
code.sub!(/\A\n/, "")
code += "\n" unless code.end_with?("\n")
code = md[1].chomp
unless @noline
if code.start_with?("\n")
code = %[#line #{@line_number + 1} "#{@filename}"#{code}\n#linereset\n]
else
code = %[#line #{@line_number} "#{@filename}"\n#{code}\n#linereset\n]
end
end
code
end
end
+1 -1
View File
@@ -1,3 +1,3 @@
class Propane
VERSION = "4.2.0"
VERSION = "4.5.0"
end
+44 -9
View File
@@ -28,7 +28,7 @@ B -> <<
b = 0;
>>
EOF
grammar = Grammar.new(input)
grammar = Grammar.new(input, "test.propane")
expect(grammar.modulename).to eq "a.b"
expect(grammar.ptype).to eq "XYZ *"
expect(grammar.ptypes).to eq("default" => "XYZ *")
@@ -62,7 +62,7 @@ EOF
expect(o).to_not be_nil
expect(o.pattern).to eq "token_with_code"
expect(o.line_number).to eq 11
expect(o.code).to eq "Code for the token\n"
expect(o.code).to eq %[#line 12 "test.propane"\nCode for the token\n#linereset\n]
o = grammar.tokens.find {|token| token.name == "token_with_no_pattern"}
expect(o).to_not be_nil
@@ -83,7 +83,7 @@ EOF
expect(o.name).to eq "A"
expect(o.components).to eq %w[B]
expect(o.line_number).to eq 19
expect(o.code).to eq " a = 42;\n"
expect(o.code).to eq %[#line 20 "test.propane"\n a = 42;\n#linereset\n]
o = grammar.rules[1]
expect(o.name).to eq "B"
@@ -95,7 +95,7 @@ EOF
expect(o.name).to eq "B"
expect(o.components).to eq []
expect(o.line_number).to eq 23
expect(o.code).to eq " b = 0;\n"
expect(o.code).to eq %[#line 24 "test.propane"\n b = 0;\n#linereset\n]
end
it "parses code segments with semicolons" do
@@ -113,7 +113,7 @@ tokenid token_with_no_pattern;
prefix myparser_;
EOF
grammar = Grammar.new(input)
grammar = Grammar.new(input, "test.propane")
expect(grammar.prefix).to eq "myparser_"
o = grammar.tokens.find {|token| token.name == "code1"}
@@ -122,7 +122,7 @@ EOF
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.code).to eq " a = b;\n return c;\n"
expect(o.code).to eq %[#line 2 "test.propane"\n a = b;\n return c;\n#linereset\n]
o = grammar.tokens.find {|token| token.name == "code2"}
expect(o).to_not be_nil
@@ -130,7 +130,42 @@ EOF
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.code).to eq %[ writeln("Hello there");\n]
expect(o.code).to eq %[#line 7 "test.propane"\n writeln("Hello there");\n#linereset\n]
end
it "does not emit #line directives with noline statement" do
input = <<EOF
noline;
token code1 <<
a = b;
return c;
>>
token code2 <<
writeln("Hello there");
>>
tokenid token_with_no_pattern;
prefix myparser_;
EOF
grammar = Grammar.new(input, "test.propane")
expect(grammar.prefix).to eq "myparser_"
o = grammar.tokens.find {|token| token.name == "code1"}
expect(o).to_not be_nil
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.code).to eq %[\n a = b;\n return c;]
o = grammar.tokens.find {|token| token.name == "code2"}
expect(o).to_not be_nil
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.code).to eq %[\n writeln("Hello there");]
end
it "supports mode labels" do
@@ -144,7 +179,7 @@ m2: /bar/ <<
drop /q/;
m3: drop /r/;
EOF
grammar = Grammar.new(input)
grammar = Grammar.new(input, "test.propane")
o = grammar.tokens.find {|token| token.name == "a"}
expect(o).to_not be_nil
@@ -197,7 +232,7 @@ tokenid int(integer);
Start (node) -> R;
R -> abc int;
EOF
grammar = Grammar.new(input)
grammar = Grammar.new(input, "test.propane")
o = grammar.tokens.find {|token| token.name == "abc"}
expect(o).to_not be_nil
+1 -1
View File
@@ -51,7 +51,7 @@ class TestLexer
end
def run(grammar, input)
grammar = Propane::Grammar.new(grammar)
grammar = Propane::Grammar.new(grammar, "test.propane")
token_dfa = Propane::Lexer::DFA.new(grammar.patterns)
test_lexer = TestLexer.new(token_dfa)
test_lexer.lex(input)
+33
View File
@@ -1781,6 +1781,39 @@ EOF
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
end
end
end
+29
View File
@@ -0,0 +1,29 @@
#include "testparser.h"
#include <assert.h>
#include <string.h>
int main()
{
p_token_info_t token_info;
char const * input = "42 f s";
p_context_t * context;
context = p_context_new((uint8_t const *)input, strlen(input));
/* Default ptype value extracted with p_value_get(). */
assert(p_lex(context, &token_info) == P_SUCCESS);
assert(token_info.token == TOKEN_num);
assert(p_value_get(&token_info.pvalue) == 42);
/* Named ptype values extracted with p_value_get_XXX(). */
assert(p_lex(context, &token_info) == P_SUCCESS);
assert(token_info.token == TOKEN_flt);
assert(p_value_get_float(&token_info.pvalue) == 1.5);
assert(p_lex(context, &token_info) == P_SUCCESS);
assert(token_info.token == TOKEN_str);
assert(strcmp(p_value_get_string(&token_info.pvalue), "hello") == 0);
p_context_delete(context);
return 0;
}
+29
View File
@@ -0,0 +1,29 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
p_token_info_t token_info;
string input = "42 f s";
p_context_t * context;
context = p_context_new(input);
/* Default ptype value extracted with p_value_get(). */
assert(p_lex(context, &token_info) == P_SUCCESS);
assert(token_info.token == TOKEN_num);
assert(p_value_get(&token_info.pvalue) == 42);
/* Named ptype values extracted with p_value_get_XXX(). */
assert(p_lex(context, &token_info) == P_SUCCESS);
assert(token_info.token == TOKEN_flt);
assert(p_value_get_float(&token_info.pvalue) == 1.5);
assert(p_lex(context, &token_info) == P_SUCCESS);
assert(token_info.token == TOKEN_str);
assert(p_value_get_string(&token_info.pvalue) == "hello");
}