Compare commits

..
71 Commits
Author SHA1 Message Date
josh d466189982 Convert Decoder.decode_code_point return type to a SumType 2023-06-29 19:21:30 -04:00
josh 9895733a05 Switch unit tests from gdc to ldc2 2023-06-29 19:16:36 -04:00
josh 11348ca351 Add FindLongestMatchResult 2023-06-04 21:14:07 -04:00
josh 6b0fb4cb12 Add return type for transition() 2023-06-03 20:38:42 -04:00
josh b78827746a Return a Token object from Lexer.user_code() 2023-03-17 20:24:48 -04:00
josh ce80e00e60 Remove TOKEN_DECODE_ERROR 2023-03-17 20:01:55 -04:00
josh 01b45df1f9 Remove TOKEN_DROP 2023-03-17 19:58:54 -04:00
josh ffccc659aa Clean up Lexer.State a bit 2023-03-14 19:38:42 -04:00
josh e432f62b05 Add UserCodeID struct to represent user code IDs 2023-03-13 21:56:18 -04:00
josh 058945e08b Add Token struct with validity checks 2023-03-13 16:14:50 -04:00
josh f402315201 Name internally generated tokens with double-underscore 2023-03-13 14:13:13 -04:00
josh d10e982fc8 Reserve double-underscore identifiers for internal use 2023-03-13 14:12:12 -04:00
josh 64974cc1e2 Refactor some of Lexer.attempt_lex_token() into find_longest_match() 2023-03-12 21:19:03 -04:00
josh b92679e0c2 Replace LexedToken with Result struct 2023-03-11 21:16:55 -05:00
josh 1af018b103 MatchInfo: store pointer to the accepting state rather than copying its fields 2023-03-09 20:16:57 -05:00
josh aabb574fea Use CodePoint type for code point values 2023-03-09 19:10:17 -05:00
josh 30004c571d Use result type query methods on Decoder.Result 2023-03-09 19:04:15 -05:00
josh b00f53050b Clean up Decoder result handling 2023-03-06 20:32:01 -05:00
josh 2e5083d823 Add explicit base types to the enums 2023-03-06 19:58:24 -05:00
josh 7f844711d9 Start on more of the user guide 2023-02-28 20:53:31 -05:00
josh e2100f6ccd Move JSON parser source to standalone file 2023-01-16 14:59:43 -05:00
josh 3057173370 Update license copyright year 2023-01-04 20:43:25 -05:00
josh 90d08e3554 Add script to build distributable propane script 2023-01-04 20:43:25 -05:00
josh f5756e9523 Add script to generate user guide HTML 2023-01-04 20:43:25 -05:00
josh dbc5560aec Add JSON parser test case 2022-11-14 23:07:10 -05:00
josh 62451f3a92 Allow multiple test files to be compiled 2022-11-14 21:00:49 -05:00
josh 2e48921bb1 Fix $n expansion for rules to use rule component ptype 2022-11-14 21:00:49 -05:00
josh c6ea4f83c2 Store parser values according to the rule/pattern type 2022-11-13 22:20:30 -05:00
josh e4a160f918 Allow defining multiple parser types and assigning parser types to tokens and rules 2022-11-13 13:35:19 -05:00
josh 8dc27686aa Rename result_type to ptype, parser result to pvalue 2022-11-12 13:29:10 -05:00
josh bca0a14371 Allow storing a result value for a token from a lexer code block 2022-10-16 21:40:25 -04:00
josh ca8a360c0e Provide matched text to lexer user code block 2022-10-15 13:37:34 -04:00
josh 623c644e74 Work on real D strings instead of ubyte pointer and length
Also fix a couple UTF-8 decoder bugs!
2022-10-15 13:32:33 -04:00
josh de93d23585 Add test for non-LALR grammar failing to generate parser 2022-10-13 05:17:06 -04:00
josh ad09ff039a Add spec to test parsing lists 2022-10-13 05:02:05 -04:00
josh 727c8cd1ea Execute rule user code blocks when reducing the rule 2022-10-13 04:56:50 -04:00
josh 74beaf7ed8 Store final parse result and give access with .result parser property 2022-10-12 21:33:09 -04:00
josh 31970522de Store parse result; add result_type grammar keyword 2022-10-12 20:56:14 -04:00
josh 02be6de48e Add lexer modes and $mode() code expansion 2022-10-09 22:49:01 -04:00
josh b2d11321fe Add grammar syntax to specify lexer mode for tokens and patterns 2022-10-04 22:23:39 -04:00
josh 6bd9d4a09b Remove _TOKEN_NONE and use _TOKEN_COUNT instead 2022-10-03 21:40:34 -04:00
josh ced5a56424 Support global user code blocks 2022-10-03 21:24:13 -04:00
josh 66d654b6b9 Add $token() user code block expansion 2022-10-02 10:43:47 -04:00
josh 43fb74fe4b Capture and verify stdout results from tests 2022-10-02 10:31:07 -04:00
josh 01ef4fc27c Rename some test files 2022-10-02 10:10:04 -04:00
josh f46b5b3f4d Remove TOKEN_EOF; define EOF token and start rule in Generator 2022-10-02 10:07:44 -04:00
josh 150be33826 Assign pattern code IDs in Generator instead of Grammar 2022-10-01 10:36:35 -04:00
josh e7e30c4f28 Add pattern statement 2022-09-30 21:05:18 -04:00
josh 04367db0ac Add forward slashes around patterns and parse more robustly 2022-09-28 23:05:01 -04:00
josh 1547528ecf Split grammar statement parsing into separate methods 2022-09-27 18:49:04 -04:00
josh 62cb5c03f2 Refactor grammar parsing a bit 2022-09-27 15:28:39 -04:00
josh 4b148f3797 Add Grammar spec 2022-09-27 14:45:29 -04:00
josh a4c5546876 Disable parser debug output 2022-09-27 12:33:23 -04:00
josh 48704295bb Rename lexer state "accepts" to "token" 2022-09-25 14:57:46 -04:00
josh 4d716f6c10 Remove Token#c_name; use given token case in token constants 2022-09-25 14:48:49 -04:00
josh 01c9340819 Rename lexer variables to reflect trying to get longest match 2022-09-25 14:44:44 -04:00
josh 672098ad32 Execute user code blocks assigned to tokens 2022-09-24 17:31:40 -04:00
josh 92ce30f354 Add tokenid keyword 2022-09-19 22:16:57 -04:00
josh 38ae5ac7a1 Split Token class into Token/Pattern 2022-09-15 22:46:44 -04:00
josh bf075a69f6 Test matching a semicolon 2022-07-23 22:25:17 -04:00
josh b682c72b17 Add semicolon to end of all grammar statements 2022-07-23 22:09:19 -04:00
josh 382e17804c Test SLR grammar 2022-06-27 21:06:03 -04:00
josh 30f4cfcc99 Write parser log file
Fix bug of skipping rule set IDs.
Remove unneeded out_sets from ItemSet class.
2022-06-26 11:06:55 -04:00
josh 2fbe13e071 Do not consume lookahead token when reducing 2022-06-25 21:35:54 -04:00
josh f2cc5b112e Handle shifting states after reducing 2022-06-25 16:16:20 -04:00
josh 84c4a16ce6 Start on Parser.parse() 2022-06-21 23:03:00 -04:00
josh f17efe8c82 Add RuleSet#id to use when reducing
Parser will know what state to go to after reducing a Rule based on the
RuleSet ID.
Start on Parser class.
2022-06-21 20:07:27 -04:00
josh 60e2818075 Determine the reduce actions for each parser state 2022-06-17 01:45:48 -04:00
josh 5af3179ff2 Add RuleSet#start_token_set 2022-06-17 01:35:02 -04:00
josh 11ea02fb86 Determine the possibly-empty RuleSets 2022-06-17 01:35:02 -04:00
josh 8f38d0b017 Clean up ItemSet#close! 2022-06-17 01:35:02 -04:00
41 changed files with 2985 additions and 373 deletions

No files matched your search

+2 -1
View File
@@ -2,7 +2,8 @@
/.yardoc
/_yardoc/
/coverage/
/doc/
/dist/
/gen/
/pkg/
/spec/reports/
/tmp/
+3
View File
@@ -0,0 +1,3 @@
## v1.0.0
- Initial release
+3
View File
@@ -2,3 +2,6 @@ source "https://rubygems.org"
gem "rake"
gem "rspec"
gem "rdoc"
gem "redcarpet"
gem "syntax"
+11 -1
View File
@@ -2,7 +2,12 @@ GEM
remote: https://rubygems.org/
specs:
diff-lcs (1.5.0)
psych (5.0.1)
stringio
rake (13.0.6)
rdoc (6.5.0)
psych (>= 4.0.0)
redcarpet (3.5.1)
rspec (3.11.0)
rspec-core (~> 3.11.0)
rspec-expectations (~> 3.11.0)
@@ -16,13 +21,18 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-support (3.11.0)
stringio (3.0.4)
syntax (1.2.2)
PLATFORMS
ruby
DEPENDENCIES
rake
rdoc
redcarpet
rspec
syntax
BUNDLED WITH
2.4.0.dev
2.3.7
+1 -1
View File
@@ -1,6 +1,6 @@
The MIT License (MIT)
Copyright (c) 2010-2022 Josh Holtrop
Copyright (c) 2010-2023 Josh Holtrop
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+9
View File
@@ -1,5 +1,9 @@
require "rspec/core/rake_task"
task :build_dist do
sh "ruby rb/build_dist.rb"
end
RSpec::Core::RakeTask.new(:spec, :example_pattern) do |task, args|
if args.example_pattern
task.rspec_opts = %W[-e "#{args.example_pattern}" -f documentation]
@@ -7,3 +11,8 @@ RSpec::Core::RakeTask.new(:spec, :example_pattern) do |task, args|
end
task :default => :spec
desc "Build user guide"
task :user_guide do
system("ruby", "-Ilib", "rb/gen_user_guide.rb")
end
+648 -115
View File
@@ -2,47 +2,95 @@
module <%= @grammar.modulename %>;
<% end %>
import std.stdio;
import std.sumtype;
<% @grammar.code_blocks.each do |code| %>
<%= code %>
<% end %>
class <%= @classname %>
{
enum
alias TokenID = uint;
enum : TokenID
{
<% @grammar.tokens.each_with_index do |token, index| %>
TOKEN_<%= token.c_name %> = <%= index %>,
TOKEN_<%= token.code_name %> = <%= index %>,
<% unless token.id == index %>
<% raise "Token ID (#{token.id}) does not match index (#{index}) for token #{token.name}!" %>
<% end %>
<% end %>
TOKEN_EOF = <%= TOKEN_EOF %>,
TOKEN_DECODE_ERROR = <%= TOKEN_DECODE_ERROR %>,
TOKEN_DROP = <%= TOKEN_DROP %>,
TOKEN_NONE = <%= TOKEN_NONE %>,
_TOKEN_COUNT = <%= @grammar.tokens.size %>,
}
static immutable string TokenNames[] = [
struct Token
{
/* Number of tokens in this parser. */
enum count = <%= @grammar.tokens.size %>;
TokenID token;
alias token this;
@disable this();
this(TokenID token)
{
this.token = token;
}
static Token invalid()
{
return Token(count);
}
bool is_valid() const
{
return token < count;
}
bool is_invalid() const
{
return !is_valid();
}
}
alias CodePoint = uint;
static immutable string[] token_names = [
<% @grammar.tokens.each_with_index do |token, index| %>
"<%= token.name %>",
<% end %>
];
static union ParserValue
{
<% @grammar.ptypes.each do |name, typestring| %>
<%= typestring %> v_<%= name %>;
<% end %>
}
static class Decoder
{
enum
struct Success
{
CODE_POINT_INVALID = 0xFFFFFFFE,
CODE_POINT_EOF = 0xFFFFFFFF,
}
struct DecodedCodePoint
{
uint code_point;
CodePoint code_point;
uint code_point_length;
}
struct EOF {}
struct DecodeError {}
static DecodedCodePoint decode_code_point(const(ubyte) * input, size_t input_length)
static SumType!(
Success,
EOF,
DecodeError)
decode_code_point(string input)
{
if (input_length == 0u)
if (input.length == 0u)
{
return DecodedCodePoint(CODE_POINT_EOF, 0u);
return cast(ReturnType!decode_code_point)EOF();
}
ubyte c = *input;
uint code_point;
char c = input[0];
CodePoint code_point;
uint code_point_length;
if ((c & 0x80u) == 0u)
{
@@ -79,25 +127,24 @@ class <%= @classname %>
}
else
{
return DecodedCodePoint(CODE_POINT_INVALID, 0u);
return cast(ReturnType!decode_code_point)DecodeError();
}
if (input_length <= following_bytes)
if (input.length <= following_bytes)
{
return DecodedCodePoint(CODE_POINT_INVALID, 0u);
return cast(ReturnType!decode_code_point)DecodeError();
}
code_point_length = following_bytes + 1u;
while (following_bytes-- > 0u)
for (size_t i = 0u; i < following_bytes; i++)
{
input++;
ubyte b = *input;
if ((b & 0xC0u) != 0u)
char b = input[i + 1u];
if ((b & 0xC0u) != 0x80u)
{
return DecodedCodePoint(CODE_POINT_INVALID, 0u);
return cast(ReturnType!decode_code_point)DecodeError();
}
code_point = (code_point << 6u) | b;
code_point = (code_point << 6u) | (b & 0x3Fu);
}
}
return DecodedCodePoint(code_point, code_point_length);
return cast(ReturnType!decode_code_point)Success(code_point, code_point_length);
}
}
@@ -105,139 +152,409 @@ class <%= @classname %>
{
private struct Transition
{
uint first;
uint last;
CodePoint first;
CodePoint last;
uint destination;
}
private struct UserCodeID
{
enum count = <%= (@grammar.patterns.map(&:code_id).compact.max || -1) + 1%>;
uint user_code_id;
alias user_code_id this;
@disable this();
this(uint user_code_id)
{
this.user_code_id = user_code_id;
}
static UserCodeID invalid()
{
return UserCodeID(count);
}
bool is_valid() const
{
return user_code_id < count;
}
bool is_invalid() const
{
return !is_valid();
}
}
private struct State
{
uint transition_table_index;
uint n_transitions;
uint accepts;
Token token;
UserCodeID code_id;
bool drop;
bool accepts() const
{
return drop || token.is_valid() || code_id.is_valid();
}
}
<% transition_table, state_table = @lexer.build_tables %>
private static const Transition transitions[] = [
private struct Mode
{
uint state_table_offset;
}
<% transition_table, state_table, mode_table = @lexer.build_tables %>
private static immutable Transition[] transitions = [
<% transition_table.each do |transition_table_entry| %>
Transition(<%= transition_table_entry[:first] %>u, <%= transition_table_entry[:last] %>u, <%= transition_table_entry[:destination] %>u),
<% end %>
];
private static const State states[] = [
private static immutable State[] states = [
<% state_table.each do |state_table_entry| %>
State(<%= state_table_entry[:transition_table_index] %>u, <%= state_table_entry[:n_transitions] %>u, <%= state_table_entry[:accepts] %>u),
State(<%= state_table_entry[:transition_table_index] %>u,
<%= state_table_entry[:n_transitions] %>u,
<% if state_table_entry[:token] %>
Token(<%= state_table_entry[:token] %>u),
<% else %>
Token.invalid(),
<% end %>
<% if state_table_entry[:code_id] %>
UserCodeID(<%= state_table_entry[:code_id] %>u),
<% else %>
UserCodeID.invalid(),
<% end %>
<%= state_table_entry[:drop] %>),
<% end %>
];
struct LexedToken
private static immutable Mode[] modes = [
<% mode_table.each do |mode_table_entry| %>
Mode(<%= mode_table_entry[:state_table_offset] %>),
<% end %>
];
struct Result
{
enum Type
{
DECODE_ERROR,
DROP,
TOKEN,
UNEXPECTED_INPUT,
}
Type type;
size_t row;
size_t col;
size_t length;
uint token;
ParserValue pvalue;
}
private const(ubyte) * m_input;
private size_t m_input_length;
private string m_input;
private size_t m_input_position;
private size_t m_input_row;
private size_t m_input_col;
private size_t m_mode;
this(const(ubyte) * input, size_t input_length)
this(string input)
{
m_input = input;
m_input_length = input_length;
m_mode = <%= @lexer.mode_id("default") %>;
}
LexedToken lex_token()
Result lex_token()
{
for (;;)
{
LexedToken lt = attempt_lex_token();
if (lt.token != TOKEN_DROP)
Result result = attempt_lex_token();
if (result.token < _TOKEN_COUNT)
{
return lt;
return result;
}
}
}
private LexedToken attempt_lex_token()
/**
* Execute user code associated with a lexer pattern.
*
* @param code_id The ID of the user code block to execute.
* @param match Matched text for this pattern.
* @param result Result lexer result in progress.
*
* @return Token to accept, or invalid token if the user code does
* not explicitly return a token.
*/
private Token user_code(UserCodeID code_id, string match, Result * result)
{
LexedToken lt = LexedToken(m_input_row, m_input_col, 0, TOKEN_NONE);
struct LexedTokenState
switch (code_id)
{
size_t length;
size_t delta_row;
size_t delta_col;
uint token;
<% @grammar.patterns.each do |pattern| %>
<% if pattern.code_id %>
case <%= pattern.code_id %>u: {
<%= expand_code(pattern.code, false, nil, pattern) %>
} break;
<% end %>
<% end %>
default: break;
}
LexedTokenState last_accepts_info;
last_accepts_info.token = TOKEN_NONE;
LexedTokenState attempt_info;
uint current_state;
return Token.invalid();
}
private Result attempt_lex_token()
{
Result result;
result.row = m_input_row;
result.col = m_input_col;
result.token = _TOKEN_COUNT;
auto match_result = find_longest_match();
if (match_result.is_eof())
{
result.type = Result.Type.TOKEN;
result.token = TOKEN___EOF;
return result;
}
else if (match_result.is_decode_error())
{
result.type = Result.Type.DECODE_ERROR;
return result;
}
else if (match_result.is_unexpected_input())
{
result.type = Result.Type.UNEXPECTED_INPUT;
return result;
}
uint token_to_accept = match_result.accepting_state.token;
if (match_result.accepting_state.code_id.is_valid())
{
Token user_code_token = user_code(match_result.accepting_state.code_id, m_input[m_input_position..(m_input_position + match_result.length)], &result);
/* An invalid Token from user_code() means that the user
* code did not explicitly return a token. So only override
* the token to return if the user code does explicitly
* return a token. */
if (user_code_token.is_valid())
{
token_to_accept = user_code_token.token;
}
}
/* Update the input position tracking. */
m_input_position += match_result.length;
m_input_row += match_result.delta_row;
if (match_result.delta_row != 0u)
{
m_input_col = match_result.delta_col;
}
else
{
m_input_col += match_result.delta_col;
}
result.token = token_to_accept;
result.length = match_result.length;
if (match_result.accepting_state.drop)
{
result.type = Result.Type.DROP;
}
else
{
result.type = Result.Type.TOKEN;
}
return result;
}
/**
* Result type for find_longest_match().
*
* Alternatives:
* - decode_error
* - eof
* - found_match(length, delta_row, delta_col, accepting_state)
* - unexpected_input(unexpected_input_length)
*/
struct FindLongestMatchResult
{
enum : ubyte
{
FOUND_MATCH,
DECODE_ERROR,
EOF,
UNEXPECTED_INPUT,
}
ubyte type;
alias type this;
union
{
struct
{
size_t length;
size_t delta_row;
size_t delta_col;
const(State) * accepting_state;
}
size_t unexpected_input_length;
}
this(ubyte type)
{
this.type = type;
}
this(ubyte type, size_t unexpected_input_length)
{
this.type = type;
this.unexpected_input_length = unexpected_input_length;
}
this(ubyte type, size_t length, size_t delta_row, size_t delta_col, const(State) * accepting_state)
{
this.type = type;
this.length = length;
this.delta_row = delta_row;
this.delta_col = delta_col;
this.accepting_state = accepting_state;
}
static FindLongestMatchResult found_match(size_t length, size_t delta_row, size_t delta_col, const(State) * accepting_state)
{
return FindLongestMatchResult(FOUND_MATCH, length, delta_row, delta_col, accepting_state);
}
static FindLongestMatchResult decode_error()
{
return FindLongestMatchResult(DECODE_ERROR);
}
static FindLongestMatchResult eof()
{
return FindLongestMatchResult(EOF);
}
static FindLongestMatchResult unexpected_input(size_t unexpected_input_length)
{
return FindLongestMatchResult(UNEXPECTED_INPUT, unexpected_input_length);
}
bool is_found_match()
{
return type == FOUND_MATCH;
}
bool is_decode_error()
{
return type == DECODE_ERROR;
}
bool is_eof()
{
return type == EOF;
}
bool is_unexpected_input()
{
return type == UNEXPECTED_INPUT;
}
}
private FindLongestMatchResult find_longest_match()
{
FindLongestMatchResult longest_match = FindLongestMatchResult.found_match(0, 0, 0, null);
FindLongestMatchResult attempt_match = longest_match;
uint current_state = modes[m_mode].state_table_offset;
for (;;)
{
auto decoded = Decoder.decode_code_point(&m_input[m_input_position + attempt_info.length], m_input_length - m_input_position - attempt_info.length);
if (decoded.code_point == Decoder.CODE_POINT_INVALID)
{
lt.token = TOKEN_DECODE_ERROR;
return lt;
}
bool lex_continue = false;
if (decoded.code_point != Decoder.CODE_POINT_EOF)
{
uint dest = transition(current_state, decoded.code_point);
if (dest != cast(uint)-1)
{
lex_continue = true;
attempt_info.length += decoded.code_point_length;
if (decoded.code_point == '\n')
{
attempt_info.delta_row++;
attempt_info.delta_col = 0u;
}
else
{
attempt_info.delta_col++;
}
current_state = dest;
if (states[current_state].accepts != TOKEN_NONE)
{
attempt_info.token = states[current_state].accepts;
last_accepts_info = attempt_info;
}
}
}
else if (attempt_info.length == 0u)
{
lt.token = TOKEN_EOF;
break;
}
if (!lex_continue)
{
if (last_accepts_info.token != TOKEN_NONE)
{
lt.token = last_accepts_info.token;
lt.length = last_accepts_info.length;
m_input_position += last_accepts_info.length;
m_input_row += last_accepts_info.delta_row;
if (last_accepts_info.delta_row != 0u)
{
m_input_col = last_accepts_info.delta_col;
}
else
{
m_input_col += last_accepts_info.delta_col;
}
}
break;
}
Decoder.decode_code_point(m_input[(m_input_position + attempt_match.length)..(m_input.length)]).match!(
(Decoder.DecodeError de) {return FindLongestMatchResult.decode_error();},
(Decoder.EOF e) {
/* We hit EOF. */
if (longest_match.length > 0)
{
/* We have a match, so use it. */
return longest_match;
}
else if (attempt_match.length != 0)
{
/* There is a partial match - error! */
return FindLongestMatchResult.unexpected_input(attempt_match.length);
}
else
{
/* Valid EOF return. */
return FindLongestMatchResult.eof();
}
},
(Decoder.Success decoded) {
auto transition_result = transition(current_state, decoded.code_point);
if (transition_result.found())
{
lex_continue = true;
attempt_match.length += decoded.code_point_length;
if (decoded.code_point == '\n')
{
attempt_match.delta_row++;
attempt_match.delta_col = 0u;
}
else
{
attempt_match.delta_col++;
}
current_state = transition_result.destination();
if (states[current_state].accepts())
{
attempt_match.accepting_state = &states[current_state];
longest_match = attempt_match;
}
}
else if (longest_match.length > 0)
{
return longest_match;
}
else
{
return FindLongestMatchResult.unexpected_input(attempt_match.length + decoded.code_point_length);
}
});
}
return lt;
}
private uint transition(uint current_state, uint code_point)
/**
* Result type for transition().
*
* Alternatives:
* - found(destination)
* - not_found
*/
struct TransitionResult
{
private uint m_destination;
static TransitionResult found(uint destination)
{
return TransitionResult(destination);
}
static TransitionResult not_found()
{
return TransitionResult(cast(uint)-1);
}
bool found()
{
return m_destination != cast(uint)-1;
}
@property uint destination()
{
return m_destination;
}
}
private TransitionResult transition(uint current_state, uint code_point)
{
uint transition_table_index = states[current_state].transition_table_index;
for (uint i = 0u; i < states[current_state].n_transitions; i++)
@@ -245,10 +562,226 @@ class <%= @classname %>
if ((transitions[transition_table_index + i].first <= code_point) &&
(code_point <= transitions[transition_table_index + i].last))
{
return transitions[transition_table_index + i].destination;
uint destination = transitions[transition_table_index + i].destination;
return TransitionResult.found(destination);
}
}
return cast(uint)-1;
return TransitionResult.not_found();
}
}
static class Parser
{
private struct Shift
{
uint symbol;
uint state;
}
private struct Reduce
{
uint token;
uint rule;
uint rule_set;
uint n_states;
}
private struct State
{
uint shift_table_index;
uint n_shift_entries;
uint reduce_table_index;
uint n_reduce_entries;
}
private struct StateValue
{
uint state;
ParserValue pvalue;
this(uint state)
{
this.state = state;
}
}
<% state_table, shift_table, reduce_table = @parser.build_tables %>
private static immutable Shift[] shifts = [
<% shift_table.each do |shift| %>
Shift(<%= shift[:token_id] %>u, <%= shift[:state_id] %>u),
<% end %>
];
private static immutable Reduce[] reduces = [
<% reduce_table.each do |reduce| %>
Reduce(<%= reduce[:token_id] %>u, <%= reduce[:rule_id] %>u, <%= reduce[:rule_set_id] %>u, <%= reduce[:n_states] %>u),
<% end %>
];
private static immutable State[] states = [
<% state_table.each do |state| %>
State(<%= state[:shift_index] %>u, <%= state[:n_shifts] %>u, <%= state[:reduce_index] %>u, <%= state[:n_reduces] %>u),
<% end %>
];
private Lexer m_lexer;
private ParserValue parse_result;
this(string input)
{
m_lexer = new Lexer(input);
}
bool parse()
{
Lexer.Result lexed_token;
uint token = _TOKEN_COUNT;
StateValue[] statevalues = new StateValue[](1);
uint reduced_rule_set = 0xFFFFFFFFu;
ParserValue reduced_parser_value;
for (;;)
{
if (token == _TOKEN_COUNT)
{
lexed_token = m_lexer.lex_token();
token = lexed_token.token;
}
uint shift_state = 0xFFFFFFFFu;
if (reduced_rule_set != 0xFFFFFFFFu)
{
shift_state = check_shift(statevalues[$-1].state, reduced_rule_set);
}
if (shift_state == 0xFFFFFFFFu)
{
shift_state = check_shift(statevalues[$-1].state, token);
if ((shift_state != 0xFFFFFFFFu) && (token == TOKEN___EOF))
{
/* Successful parse. */
parse_result = statevalues[$-1].pvalue;
return true;
}
}
if (shift_state != 0xFFFFFFFFu)
{
/* We have something to shift. */
statevalues ~= StateValue(shift_state);
if (reduced_rule_set == 0xFFFFFFFFu)
{
/* We shifted a token, mark it consumed. */
token = _TOKEN_COUNT;
statevalues[$-1].pvalue = lexed_token.pvalue;
}
else
{
/* We shifted a RuleSet. */
statevalues[$-1].pvalue = reduced_parser_value;
ParserValue new_parse_result;
reduced_parser_value = new_parse_result;
reduced_rule_set = 0xFFFFFFFFu;
}
continue;
}
uint reduce_index = check_reduce(statevalues[$-1].state, token);
if (reduce_index != 0xFFFFFFFFu)
{
/* We have something to reduce. */
reduced_parser_value = user_code(reduces[reduce_index].rule, statevalues, reduces[reduce_index].n_states);
reduced_rule_set = reduces[reduce_index].rule_set;
statevalues.length -= reduces[reduce_index].n_states;
continue;
}
/* Error, unexpected token. */
write("Unexpected token ");
if (token < _TOKEN_COUNT)
{
writeln(token_names[token]);
}
else
{
writeln("{other}");
}
return false;
}
}
@property <%= start_rule_type[1] %> result()
{
return parse_result.v_<%= start_rule_type[0] %>;
}
private uint check_shift(uint state, uint symbol)
{
uint start = states[state].shift_table_index;
uint end = start + states[state].n_shift_entries;
for (uint i = start; i < end; i++)
{
if (shifts[i].symbol == symbol)
{
// if (symbol < _TOKEN_COUNT)
// {
// writeln("Shifting ", token_names[symbol]);
// }
// else
// {
// writeln("Shifting rule set ", symbol);
// }
return shifts[i].state;
}
}
return 0xFFFFFFFFu;
}
private uint check_reduce(uint state, uint token)
{
uint start = states[state].reduce_table_index;
uint end = start + states[state].n_reduce_entries;
for (uint i = start; i < end; i++)
{
if ((reduces[i].token == token) ||
(reduces[i].token == _TOKEN_COUNT))
{
// write("Reducing rule ", reduces[i].rule, ", rule set ", reduces[i].rule_set, " lookahead ");
// if (token < _TOKEN_COUNT)
// {
// writeln(token_names[token]);
// }
// else
// {
// writeln("{other}");
// }
return i;
}
}
return 0xFFFFFFFFu;
}
/**
* Execute user code associated with a parser rule.
*
* @param rule The ID of the rule.
*
* @return Parse value.
*/
private ParserValue user_code(uint rule, StateValue[] statevalues, uint n_states)
{
ParserValue _pvalue;
switch (rule)
{
<% @grammar.rules.each do |rule| %>
<% if rule.code %>
case <%= rule.id %>u: {
<%= expand_code(rule.code, true, rule, nil) %>
} break;
<% end %>
<% end %>
default: break;
}
return _pvalue;
}
}
}
+76
View File
@@ -0,0 +1,76 @@
${remove}
WARNING: This user guide is meant to be preprocessed and rendered by a custom
script.
The markdown source file is not intended to be viewed directly and will not
include all intended content.
${/remove}
#> Overview
Propane is an LR Parser Generator (LPG) which:
* accepts LR(0), SLR, and LALR grammars
* generates a built-in lexer to tokenize input
* supports UTF-8 lexer inputs
* generates a table-driven parser to parse input in linear time
* is MIT-licensed
* is distributable as a standalone Ruby script
* supports D language
#> Installation
Propane is designed to be distributed as a stand-alone single file script that
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
installed.
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
the project to be built (typically the root of the repository) and mark it
executable.
#> Command Line Usage
Propane is typically invoked from the command-line as `./propane`.
Usage: ./propane [options] <input-file> <output-file>
Options:
--log LOG Write log file
--version Show program version and exit
-h, --help Show this usage and exit
The user must specify the path to a Propane input grammar file and a path to an
output file.
The generated source code will be written to the output file.
If a log file path is specified, Propane will write a log file containing
detailed information about the parser states and transitions.
#> Propane Grammar File
A Propane grammar file provides Propane with the patterns, tokens, grammar
rules, and user code blocks from which to build the generated lexer and parser.
#> License
Propane is licensed under the terms of the MIT License:
```
${include LICENSE.txt}
```
#> Contributing
Propane is developed on [github](https://github.com/holtrop/propane).
Issues may be submitted to [https://github.com/holtrop/propane/issues](https://github.com/holtrop/propane/issues).
Pull requests may be submitted as well:
1. Fork it
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create new Pull Request
#> Change Log
${changelog}
+3 -12
View File
@@ -1,5 +1,6 @@
require "erb"
require "set"
require "stringio"
require_relative "propane/cli"
require_relative "propane/code_point_range"
require_relative "propane/fa"
@@ -12,28 +13,18 @@ require_relative "propane/lexer/dfa"
require_relative "propane/parser"
require_relative "propane/parser/item"
require_relative "propane/parser/item_set"
require_relative "propane/pattern"
require_relative "propane/regex"
require_relative "propane/regex/nfa"
require_relative "propane/regex/unit"
require_relative "propane/rule_set"
require_relative "propane/rule"
require_relative "propane/token"
require_relative "propane/util"
require_relative "propane/version"
class Propane
# EOF.
TOKEN_EOF = 0xFFFFFFFC
# Decoding error.
TOKEN_DECODE_ERROR = 0xFFFFFFFD
# Token ID for a "dropped" token.
TOKEN_DROP = 0xFFFFFFFE
# Invalid token ID.
TOKEN_NONE = 0xFFFFFFFF
class Error < RuntimeError
end
+118 -9
View File
@@ -5,7 +5,11 @@ class Propane
def initialize(grammar, output_file, log_file)
@grammar = grammar
@output_file = output_file
@log_file = log_file
if log_file
@log = File.open(log_file, "wb")
else
@log = StringIO.new
end
@classname = @grammar.classname || File.basename(output_file).sub(%r{[^a-zA-Z0-9].*}, "").capitalize
process_grammar!
end
@@ -16,29 +20,80 @@ class Propane
File.open(@output_file, "wb") do |fh|
fh.write(result)
end
@log.close
end
private
def process_grammar!
# Assign default pattern mode to patterns without a mode assigned.
found_default = false
@grammar.patterns.each do |pattern|
if pattern.mode.nil?
pattern.mode = "default"
found_default = true
end
pattern.ptypename ||= "default"
end
unless found_default
raise Error.new("No patterns found for default mode")
end
# Add EOF token.
@grammar.tokens << Token.new("$EOF", nil, nil)
tokens_by_name = {}
@grammar.tokens.each do |token|
@grammar.tokens.each_with_index do |token, token_id|
# Assign token ID.
token.id = token_id
# Set default ptypename if none given.
token.ptypename ||= "default"
# Check for token name conflicts.
if tokens_by_name.include?(token.name)
raise Error.new("Duplicate token name #{token.name.inspect}")
end
tokens_by_name[token.name] = token
end
# Check for user start rule.
unless @grammar.rules.find {|rule| rule.name == "Start"}
raise Error.new("Start rule not found")
end
# Add "real" start rule.
@grammar.rules.unshift(Rule.new("$Start", ["Start", "$EOF"], nil, nil, nil))
rule_sets = {}
@grammar.rules.each do |rule|
rule_set_id = @grammar.tokens.size
@grammar.rules.each_with_index do |rule, rule_id|
# Assign rule ID.
rule.id = rule_id
# Check for token/rule name conflict.
if tokens_by_name.include?(rule.name)
raise Error.new("Rule name collides with token name #{rule.name.inspect}")
end
rule_sets[rule.name] ||= RuleSet.new(rule.name)
rule_sets[rule.name] << rule
# Build rule sets of all rules with the same name.
unless rule_sets[rule.name]
rule_sets[rule.name] = RuleSet.new(rule.name, rule_set_id)
rule_set_id += 1
end
rule_set = rule_sets[rule.name]
if rule_set.ptypename && rule.ptypename && rule_set.ptypename != rule.ptypename
raise Error.new("Conflicting ptypes for rule #{rule.name}")
end
rule_set.ptypename ||= rule.ptypename
rule.rule_set = rule_set
rule_set << rule
end
unless rule_sets["Start"]
raise Error.new("Start rule not found")
rule_sets.each do |name, rule_set|
rule_set.ptypename ||= "default"
# Assign rule set ptypenames back to rules.
rule_set.rules.each do |rule|
rule.ptypename = rule_set.ptypename
end
end
# Generate lexer user code IDs for lexer patterns with user code blocks.
@grammar.patterns.select do |pattern|
pattern.code
end.each_with_index do |pattern, code_id|
pattern.code_id = code_id
end
# Map rule components from names to Token/RuleSet objects.
@grammar.rules.each do |rule|
rule.components.map! do |component|
if tokens_by_name[component]
@@ -51,8 +106,10 @@ class Propane
end
end
determine_possibly_empty_rulesets!(rule_sets)
@lexer = Lexer.new(@grammar.tokens, @grammar.drop_tokens)
@parser = Parser.new(rule_sets["Start"])
# Generate the lexer.
@lexer = Lexer.new(@grammar)
# Generate the parser.
@parser = Parser.new(@grammar, rule_sets, @log)
end
# Determine which grammar rules could expand to empty sequences.
@@ -111,6 +168,58 @@ class Propane
end
end
# Expand expansions in user code block.
#
# @param code [String]
# User code block.
# @param parser [Boolean]
# Whether the user code is for the parser or lexer.
# @param rule [Rule, nil]
# The Rule associated with the user code if user code is for the parser.
# @param pattern [Pattern, nil]
# The Pattern associated with the user code if user code is for the lexer.
#
# @return [String]
# Expanded user code block.
def expand_code(code, parser, rule, pattern)
code = code.gsub(/\$token\(([$\w]+)\)/) do |match|
"Token(TOKEN_#{Token.code_name($1)})"
end
if parser
code = code.gsub(/\$\$/) do |match|
"_pvalue.v_#{rule.ptypename}"
end
code = code.gsub(/\$(\d+)/) do |match|
index = $1.to_i
"statevalues[$-1-n_states+#{index}].pvalue.v_#{rule.components[index - 1].ptypename}"
end
else
code = code.gsub(/\$\$/) do |match|
"result.pvalue.v_#{pattern.ptypename}"
end
code = code.gsub(/\$mode\(([a-zA-Z_][a-zA-Z_0-9]*)\)/) do |match|
mode_name = $1
mode_id = @lexer.mode_id(mode_name)
unless mode_id
raise Error.new("Lexer mode '#{mode_name}' not found")
end
"m_mode = #{mode_id}u"
end
end
code
end
# Get the parser value type for the start rule.
#
# @return [Array<String>]
# Start rule parser value type name and type string.
def start_rule_type
start_rule = @grammar.rules.find do |rule|
rule.name == "Start"
end
[start_rule.ptypename, @grammar.ptypes[start_rule.ptypename]]
end
end
end
+226 -41
View File
@@ -2,60 +2,245 @@ class Propane
class Grammar
# Reserve identifiers beginning with a double-underscore for internal use.
IDENTIFIER_REGEX = /(?:[a-zA-Z]|_[a-zA-Z0-9])[a-zA-Z_0-9]*/
attr_reader :classname
attr_reader :drop_tokens
attr_reader :modulename
attr_reader :patterns
attr_reader :rules
attr_reader :tokens
attr_reader :code_blocks
attr_reader :ptypes
def initialize(input)
@patterns = []
@tokens = []
@drop_tokens = []
@rules = []
input = input.gsub("\r\n", "\n")
parse_grammar(input)
@code_blocks = []
@line_number = 1
@next_line_number = @line_number
@mode = nil
@input = input.gsub("\r\n", "\n")
@ptypes = {"default" => "void *"}
parse_grammar!
end
def ptype
@ptypes["default"]
end
private
def parse_grammar(input)
line_number = 1
while !input.empty?
if sliced = input.slice!(/\A\s+/)
# Skip white space.
elsif sliced = input.slice!(/\A#.*\n/)
# Skip comment lines.
elsif sliced = input.slice!(/\Amodule\s+(\S+)\n/)
@modulename = $1
elsif sliced = input.slice!(/\Aclass\s+(\S+)\n/)
@classname = $1
elsif sliced = input.slice!(/\Atoken\s+(\S+)(?:\s+(\S+))?\n/)
name, pattern = $1, $2
if pattern.nil?
pattern = name
end
unless name =~ /^[a-zA-Z_][a-zA-Z_0-9]*$/
raise Error.new("Invalid token name #{name.inspect}")
end
@tokens << Token.new(name: name, pattern: pattern, id: @tokens.size, line_number: line_number)
elsif sliced = input.slice!(/\Adrop\s+(\S+)\n/)
pattern = $1
@drop_tokens << Token.new(pattern: pattern, line_number: line_number)
elsif sliced = input.slice!(/\A(\S+)\s*->\s*(.*?)(?:;|<<\n(.*?)^>>\n)/m)
rule_name, components, code = $1, $2, $3
unless rule_name =~ /^[a-zA-Z_][a-zA-Z_0-9]*$/
raise Error.new("Invalid rule name #{name.inspect}")
end
components = components.strip.split(/\s+/)
# Reserve rule ID 0 for the "real" start rule.
@rules << Rule.new(rule_name, components, code, line_number, @rules.size + 1)
else
if input.size > 25
input = input.slice(0..20) + "..."
end
raise Error.new("Unexpected grammar input at line #{line_number}: #{input.chomp}")
def parse_grammar!
while @input.size > 0
parse_statement!
end
end
def parse_statement!
if parse_white_space!
elsif parse_comment_line!
elsif @mode.nil? && parse_mode_label!
elsif parse_module_statement!
elsif parse_class_statement!
elsif parse_ptype_statement!
elsif parse_pattern_statement!
elsif parse_token_statement!
elsif parse_tokenid_statement!
elsif parse_drop_statement!
elsif parse_rule_statement!
elsif parse_code_block_statement!
else
if @input.size > 25
@input = @input.slice(0..20) + "..."
end
line_number += sliced.count("\n")
raise Error.new("Unexpected grammar input at line #{@line_number}: #{@input.chomp}")
end
end
def parse_mode_label!
if md = consume!(/(#{IDENTIFIER_REGEX})\s*:/)
@mode = md[1]
end
end
def parse_white_space!
consume!(/\s+/)
end
def parse_comment_line!
consume!(/#.*\n/)
end
def parse_module_statement!
if consume!(/module\s+/)
md = consume!(/([\w.]+)\s*/, "expected module name")
@modulename = md[1]
consume!(/;/, "expected `;'")
@mode = nil
true
end
end
def parse_class_statement!
if consume!(/class\s+/)
md = consume!(/([\w.]+)\s*/, "expected class name")
@classname = md[1]
consume!(/;/, "expected `;'")
@mode = nil
true
end
end
def parse_ptype_statement!
if consume!(/ptype\s+/)
name = "default"
if md = consume!(/(#{IDENTIFIER_REGEX})\s*=\s*/)
name = md[1]
end
md = consume!(/([^;]+);/, "expected parser result type expression")
@ptypes[name] = md[1].strip
end
end
def parse_token_statement!
if consume!(/token\s+/)
md = consume!(/(#{IDENTIFIER_REGEX})\s*/, "expected token name")
name = md[1]
if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/)
ptypename = md[1]
end
pattern = parse_pattern! || name
consume!(/\s+/)
unless code = parse_code_block!
consume!(/;/, "expected pattern or `;' or code block")
end
token = Token.new(name, ptypename, @line_number)
@tokens << token
pattern = Pattern.new(pattern: pattern, token: token, line_number: @line_number, code: code, mode: @mode, ptypename: ptypename)
@patterns << pattern
@mode = nil
true
end
end
def parse_tokenid_statement!
if md = consume!(/tokenid\s+/)
md = consume!(/(#{IDENTIFIER_REGEX})\s*/, "expected token name")
name = md[1]
if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/)
ptypename = md[1]
end
consume!(/;/, "expected `;'");
token = Token.new(name, ptypename, @line_number)
@tokens << token
@mode = nil
true
end
end
def parse_drop_statement!
if md = consume!(/drop\s+/)
pattern = parse_pattern!
unless pattern
raise Error.new("Line #{@line_number}: expected pattern to follow `drop'")
end
consume!(/\s+/)
consume!(/;/, "expected `;'")
@patterns << Pattern.new(pattern: pattern, line_number: @line_number, drop: true, mode: @mode)
@mode = nil
true
end
end
def parse_rule_statement!
if md = consume!(/(#{IDENTIFIER_REGEX})\s*(?:\((#{IDENTIFIER_REGEX})\))?\s*->\s*/)
rule_name, ptypename = *md[1, 2]
md = consume!(/((?:#{IDENTIFIER_REGEX}\s*)*)\s*/, "expected rule component list")
components = md[1].strip.split(/\s+/)
unless code = parse_code_block!
consume!(/;/, "expected pattern or `;' or code block")
end
@rules << Rule.new(rule_name, components, code, ptypename, @line_number)
@mode = nil
true
end
end
def parse_pattern_statement!
if pattern = parse_pattern!
consume!(/\s+/)
if md = consume!(/\((#{IDENTIFIER_REGEX})\)\s*/)
ptypename = md[1]
end
unless code = parse_code_block!
raise Error.new("Line #{@line_number}: expected code block to follow pattern")
end
@patterns << Pattern.new(pattern: pattern, line_number: @line_number, code: code, mode: @mode, ptypename: ptypename)
@mode = nil
true
end
end
def parse_code_block_statement!
if code = parse_code_block!
@code_blocks << code
@mode = nil
true
end
end
def parse_pattern!
if md = consume!(%r{/})
pattern = ""
while !consume!(%r{/})
if consume!(%r{\\})
pattern += "\\"
if md = consume!(%r{(.)})
pattern += md[1]
else
raise Error.new("Line #{@line_number}: unterminated escape sequence")
end
elsif md = consume!(%r{(.)})
pattern += md[1]
end
end
pattern
end
end
def parse_code_block!
if md = consume!(/<<\n(.*?)^>>\n/m)
md[1]
end
end
# Check if the input string matches the given regex.
#
# If so, remove the match from the input string, and update the line
# number. If the regex is not matched and an error message is provided,
# the error is raised.
#
# @param regex [Regexp]
# Regex to attempt to match.
# @param error_message [String, nil]
# Error message to display if the regex is not matched. If nil and the
# regex is not matched, an error is not raised.
#
# @return [MatchData, nil]
# MatchData for the given regex if it was matched and removed from the
# input.
def consume!(regex, error_message = nil)
@line_number = @next_line_number
if md = @input.match(/\A#{regex}/)
@input.slice!(0, md[0].size)
@next_line_number += md[0].count("\n")
md
elsif error_message
raise Error.new("Line #{@line_number}: Error: #{error_message}")
else
false
end
end
+53 -26
View File
@@ -1,41 +1,68 @@
class Propane
class Lexer
# @return [DFA]
# Lexer DFA.
attr_accessor :dfa
def initialize(tokens, drop_tokens)
@dfa = DFA.new(tokens, drop_tokens)
def initialize(grammar)
@grammar = grammar
end
def build_tables
@modes = @grammar.patterns.group_by do |pattern|
pattern.mode
end.transform_values do |patterns|
{dfa: DFA.new(patterns)}
end
@modes.each_with_index do |(mode_name, mode_info), index|
mode_info[:id] = index
end
transition_table = []
state_table = []
states = @dfa.enumerate
states.each do |state, id|
accepts =
if state.accepts.nil?
TOKEN_NONE
elsif state.accepts.drop?
TOKEN_DROP
else
state.accepts.id
end
state_table << {
transition_table_index: transition_table.size,
n_transitions: state.transitions.size,
accepts: accepts,
mode_table = []
@modes.each do |mode_name, mode_info|
state_table_offset = state_table.size
mode_table << {
state_table_offset: state_table_offset,
}
state.transitions.each do |transition|
transition_table << {
first: transition.code_point_range.first,
last: transition.code_point_range.last,
destination: states[transition.destination],
states = mode_info[:dfa].enumerate
states.each do |state, id|
drop = state.accepts && state.accepts.drop?
token =
if state.accepts && state.accepts.token
state.accepts.token.id
end
code_id =
if state.accepts && state.accepts.code_id
state.accepts.code_id
end
state_table << {
transition_table_index: transition_table.size,
n_transitions: state.transitions.size,
drop: drop,
token: token,
code_id: code_id,
}
state.transitions.each do |transition|
transition_table << {
first: transition.code_point_range.first,
last: transition.code_point_range.last,
destination: states[transition.destination] + state_table_offset,
}
end
end
end
[transition_table, state_table]
[transition_table, state_table, mode_table]
end
# Get ID for a mode.
#
# @param mode_name [String]
# Mode name.
#
# @return [Integer, nil]
# Mode ID.
def mode_id(mode_name)
if mode_info = @modes[mode_name]
mode_info[:id]
end
end
end
+4 -4
View File
@@ -3,11 +3,11 @@ class Propane
class DFA < FA
def initialize(tokens, drop_tokens)
def initialize(patterns)
super()
start_nfa = Regex::NFA.new
(tokens + drop_tokens).each do |token|
start_nfa.start_state.add_transition(nil, token.nfa.start_state)
patterns.each do |pattern|
start_nfa.start_state.add_transition(nil, pattern.nfa.start_state)
end
@nfa_state_sets = {}
@states = []
@@ -40,7 +40,7 @@ class Propane
nfa_state_set.each do |nfa_state|
if nfa_state.accepts
if state.accepts
if nfa_state.accepts.id < state.accepts.id
if nfa_state.accepts.line_number < state.accepts.line_number
state.accepts = nfa_state.accepts
end
else
+198 -21
View File
@@ -2,12 +2,13 @@ class Propane
class Parser
def initialize(start_rule_set)
@eof_token = Token.new(name: "$", id: TOKEN_EOF)
start_rule = Rule.new("$$", [start_rule_set, @eof_token], nil, nil, 0)
def initialize(grammar, rule_sets, log)
@grammar = grammar
@rule_sets = rule_sets
@log = log
@item_sets = []
@item_sets_set = {}
start_item = Item.new(start_rule, 0)
start_item = Item.new(grammar.rules.first, 0)
eval_item_sets = Set[ItemSet.new([start_item])]
while eval_item_sets.size > 0
@@ -18,7 +19,7 @@ class Propane
@item_sets << item_set
@item_sets_set[item_set] = item_set
item_set.following_symbols.each do |following_symbol|
unless following_symbol == @eof_token
unless following_symbol.name == "$EOF"
following_set = item_set.build_following_item_set(following_symbol)
eval_item_sets << following_set
end
@@ -28,45 +29,59 @@ class Propane
@item_sets.each do |item_set|
process_item_set(item_set)
puts "Item set #{item_set.id}:"
ids = item_set.in_sets.map(&:id)
if ids.size > 0
puts " (in from #{ids.join(", ")})"
end
puts item_set
item_set.following_item_set.each do |following_symbol, following_item_set|
puts " #{following_symbol.name} => #{following_item_set.id}"
end
puts
end
build_reduce_actions!
write_log!
end
def build_tables
shift_table = []
state_table = []
reduce_table = []
@item_sets.each do |item_set|
shift_entries = item_set.following_symbols.select do |following_symbol|
following_symbol.is_a?(Token)
end.map do |following_symbol|
shift_entries = item_set.following_symbols.map do |following_symbol|
state_id =
if following_symbol.name == "$EOF"
0
else
item_set.following_item_set[following_symbol].id
end
{
token_id: following_symbol.id,
state_id: item_set.following_item_set[following_symbol].id,
state_id: state_id,
}
end
reduce_entries =
case ra = item_set.reduce_actions
when Rule
[{token_id: @grammar.tokens.size, rule_id: ra.id,
rule_set_id: ra.rule_set.id, n_states: ra.components.size}]
when Hash
ra.map do |token, rule|
{token_id: token.id, rule_id: rule.id,
rule_set_id: rule.rule_set.id, n_states: rule.components.size}
end
else
[]
end
state_table << {
shift_index: shift_table.size,
n_shifts: shift_entries.size,
reduce_index: reduce_table.size,
n_reduces: reduce_entries.size,
}
shift_table += shift_entries
reduce_table += reduce_entries
end
[state_table, shift_table]
[state_table, shift_table, reduce_table]
end
private
def process_item_set(item_set)
item_set.following_symbols.each do |following_symbol|
unless following_symbol == @eof_token
unless following_symbol.name == "$EOF"
following_set = @item_sets_set[item_set.build_following_item_set(following_symbol)]
item_set.following_item_set[following_symbol] = following_set
following_set.in_sets << item_set
@@ -74,6 +89,168 @@ class Propane
end
end
# Build the reduce actions for each ItemSet.
#
# @return [void]
def build_reduce_actions!
@item_sets.each do |item_set|
item_set.reduce_actions = build_reduce_actions_for_item_set(item_set)
end
end
# Build the reduce actions for a single item set (parser state).
#
# @param item_set [ItemSet]
# ItemSet (parser state)
#
# @return [nil, Rule, Hash]
# If no reduce actions are possible for the given item set, nil.
# If only one reduce action is possible for the given item set, the Rule
# to reduce.
# Otherwise, a mapping of lookahead Tokens to the Rules to reduce.
def build_reduce_actions_for_item_set(item_set)
# To build the reduce actions, we start by looking at any
# "complete" items, i.e., items where the parse position is at the
# end of a rule. These are the only rules that are candidates for
# reduction in the current ItemSet.
reduce_rules = Set.new(item_set.items.select(&:complete?).map(&:rule))
# If there are no rules to reduce for this ItemSet, we're done here.
return nil if reduce_rules.size == 0
# If there is exactly one rule to reduce for this ItemSet, then do not
# figure out the lookaheads; just reduce it.
return reduce_rules.first if reduce_rules.size == 1
# Otherwise, we have more than one possible rule to reduce.
# We will be looking for all possible tokens that can follow instances of
# these rules. Rather than looking through the entire grammar for the
# possible following tokens, we will only look in the item sets leading
# up to this one. This restriction gives us a more precise lookahead set,
# and allows us to parse LALR grammars.
item_sets = item_set.leading_item_sets
reduce_rules.reduce({}) do |reduce_actions, reduce_rule|
lookahead_tokens_for_rule = build_lookahead_tokens_to_reduce(reduce_rule, item_sets)
lookahead_tokens_for_rule.each do |lookahead_token|
if existing_reduce_rule = reduce_actions[lookahead_token]
raise Error.new("Error: reduce/reduce conflict between rule #{existing_reduce_rule.id} (#{existing_reduce_rule.name}) and rule #{reduce_rule.id} (#{reduce_rule.name})")
end
reduce_actions[lookahead_token] = reduce_rule
end
reduce_actions
end
end
# Build the set of lookahead Tokens that should cause the given Rule to be
# reduced in the given context of ItemSets.
#
# @param rule [Rule]
# Rule to reduce.
# @param item_sets [Set<ItemSet>]
# ItemSets to consider for the context in which to reduce this Rule.
#
# @return [Set<Token>]
# Possible lookahead Tokens for the given Rule within the context of the
# given ItemSets.
def build_lookahead_tokens_to_reduce(rule, item_sets)
# We need to look for possible following tokens for this reduce rule. We
# do this by looking for tokens that follow the reduce rule, or the
# start token set for any other rule that follows the reduce rule.
# While doing this, the following situations could arise:
# 1. We may come across a following rule that could be empty. In this
# case, in addition to the start token set for that rule, we must also
# continue to the next following symbol after the potentially empty
# rule and continue the search for potential following tokens.
# 2. We may reach the end of a rule that was not one of the original
# reduce rules. In this case, we must also search for all potential
# following tokens for this rule as well.
lookahead_tokens = Set.new
rule_sets_to_check_after = [rule.rule_set]
checked_rule_sets = Set.new
while !rule_sets_to_check_after.empty?
rule_set = rule_sets_to_check_after.slice!(0)
checked_rule_sets << rule_set
# For each RuleSet we're checking, we're going to look through all
# items in the item sets of interest and gather all possible following
# tokens to form the lookahead token set.
item_sets.each do |item_set|
item_set.items.each do |item|
if item.following_symbol == rule_set
(1..).each do |offset|
case symbol = item.following_symbol(offset)
when nil
rule_set = item.rule.rule_set
unless checked_rule_sets.include?(rule_set)
rule_sets_to_check_after << rule_set
end
break
when Token
lookahead_tokens << symbol
break
when RuleSet
lookahead_tokens += symbol.start_token_set
unless symbol.could_be_empty?
break
end
end
end
end
end
end
end
lookahead_tokens
end
def write_log!
@log.puts Util.banner("Parser Rules")
@grammar.rules.each do |rule|
@log.puts
@log.puts "Rule #{rule.id}:"
@log.puts " #{rule}"
end
@log.puts
@log.puts Util.banner("Parser Tokens")
@log.puts
@grammar.tokens.each do |token|
@log.puts "Token #{token.id}: #{token.name}"
end
@log.puts
@log.puts Util.banner("Parser Rule Sets")
@rule_sets.each do |rule_set_name, rule_set|
@log.puts
@log.puts "Rule Set #{rule_set.id}: #{rule_set_name}"
@log.puts " Start token set: #{rule_set.start_token_set.map(&:name).join(", ")}"
end
@log.puts
@log.puts Util.banner("Parser States")
@item_sets.each do |item_set|
@log.puts
@log.puts "State #{item_set.id}:"
@log.puts item_set.to_s.gsub(/^/, " ")
incoming_ids = item_set.in_sets.map(&:id)
@log.puts
@log.puts " Incoming states: #{incoming_ids.join(", ")}"
@log.puts " Outgoing states:"
item_set.following_item_set.each do |following_symbol, following_item_set|
@log.puts " #{following_symbol.name} => #{following_item_set.id}"
end
@log.puts
@log.puts " Reduce actions:"
case item_set.reduce_actions
when Rule
@log.puts " * => #{item_set.reduce_actions.id} (#{item_set.reduce_actions.name})"
when Hash
item_set.reduce_actions.each do |token, rule|
@log.puts " lookahead #{token.name} => #{rule.name} (#{rule.id}), rule set ##{rule.rule_set.id}"
end
end
end
end
end
end
+18 -2
View File
@@ -86,10 +86,26 @@ class Propane
# That is, the symbol which follows the parse position marker in the
# current Item.
#
# @param offset [Integer]
# Offset from current parse position to examine.
#
# @return [Token, RuleSet, nil]
# Following symbol for the Item.
def following_symbol
@rule.components[@position]
def following_symbol(offset = 0)
@rule.components[@position + offset]
end
# Get the previous symbol for the Item.
#
# That is, the symbol which precedes the parse position marker in the
# current Item.
#
# @return [Token, RuleSet, nil]
# Previous symbol for this Item.
def previous_symbol
if @position > 0
@rule.components[@position - 1]
end
end
# Get whether this Item is followed by the provided symbol.
+5 -1
View File
@@ -21,6 +21,10 @@ class Propane
# ItemSets leading to this item set.
attr_reader :in_sets
# @return [nil, Rule, Hash]
# Reduce actions, mapping lookahead tokens to rules.
attr_accessor :reduce_actions
# Build an ItemSet.
#
# @param items [Array<Item>]
@@ -110,7 +114,7 @@ class Propane
#
# @return [void]
def close!
eval_items = @items
eval_items = @items.dup
while eval_items.size > 0
item = eval_items.first
eval_items.delete(item)
+76
View File
@@ -0,0 +1,76 @@
class Propane
class Pattern
# @return [String, nil]
# Code block to execute when the pattern is matched.
attr_reader :code
# @option options [Integer, nil] :code_id
# Code block ID.
attr_accessor :code_id
# @return [String, nil]
# Pattern.
attr_reader :pattern
# @return [Token, nil]
# Token to be returned by this pattern.
attr_reader :token
# @return [Integer, nil]
# Line number where the pattern was defined in the input grammar.
attr_reader :line_number
# @return [Regex::NFA, nil]
# Regex NFA for matching the pattern.
attr_reader :nfa
# @return [String, nil]
# Lexer mode for this pattern.
attr_accessor :mode
# @return [String, nil]
# Parser value type name.
attr_accessor :ptypename
# Construct a Pattern.
#
# @param options [Hash]
# Optional parameters.
# @option options [String, nil] :code
# Code block to execute when the pattern is matched.
# @option options [Boolean] :drop
# Whether this is a drop pattern.
# @option options [String, nil] :pattern
# Pattern.
# @option options [Token, nil] :token
# Token to be returned by this pattern.
# @option options [Integer, nil] :line_number
# Line number where the token was defined in the input grammar.
# @option options [String, nil] :mode
# Lexer mode for this pattern.
def initialize(options)
@code = options[:code]
@drop = options[:drop]
@pattern = options[:pattern]
@token = options[:token]
@line_number = options[:line_number]
@mode = options[:mode]
@ptypename = options[:ptypename]
regex = Regex.new(@pattern)
regex.nfa.end_state.accepts = self
@nfa = regex.nfa
end
# Whether the pattern is a drop pattern.
#
# @return [Boolean]
# Whether the pattern is a drop pattern.
def drop?
@drop
end
end
end
+21 -5
View File
@@ -12,7 +12,11 @@ class Propane
# @return [Integer]
# Rule ID.
attr_reader :id
attr_accessor :id
# @return [String, nil]
# Parser type name.
attr_accessor :ptypename
# @return [Integer]
# Line number where the rule was defined in the input grammar.
@@ -22,6 +26,10 @@ class Propane
# Rule name.
attr_reader :name
# @return [RuleSet]
# The RuleSet that this Rule is a part of.
attr_accessor :rule_set
# Construct a Rule.
#
# @param name [String]
@@ -30,15 +38,15 @@ class Propane
# Rule components.
# @param code [String]
# User code associated with the rule.
# @param ptypename [String, nil]
# Parser type name.
# @param line_number [Integer]
# Line number where the rule was defined in the input grammar.
# @param id [Integer]
# Rule ID.
def initialize(name, components, code, line_number, id)
def initialize(name, components, code, ptypename, line_number)
@name = name
@components = components
@code = code
@id = id
@ptypename = ptypename
@line_number = line_number
end
@@ -52,6 +60,14 @@ class Propane
@components.empty?
end
# Represent the Rule as a String.
#
# @return [String]
# Rule represented as a String.
def to_s
"#{@name} -> #{@components.map(&:name).join(" ")}"
end
end
end
+12 -1
View File
@@ -2,10 +2,18 @@ class Propane
class RuleSet
# @return [Integer]
# ID of the RuleSet.
attr_accessor :id
# @return [String]
# Name of the RuleSet.
attr_reader :name
# @return [String, nil]
# Parser type name.
attr_accessor :ptypename
# @return [Array<Rule>]
# Rules in the RuleSet.
attr_reader :rules
@@ -18,7 +26,10 @@ class Propane
#
# @param name [String]
# Name of the RuleSet.
def initialize(name)
# @param id [Integer]
# ID of the RuleSet.
def initialize(name, id)
@id = id
@name = name
@rules = []
@could_be_empty = false
+28 -34
View File
@@ -2,60 +2,54 @@ class Propane
class Token
class << self
# Name of the token to use in code (special characters replaced).
#
# @return [String]
# Name of the token to use in code (special characters replaced).
def code_name(name)
name.sub(/^\$/, "__")
end
end
# @return [String, nil]
# Token name.
attr_reader :name
# @return [String, nil]
# Token pattern.
attr_reader :pattern
# Parser value type name.
attr_accessor :ptypename
# @return [Integer, nil]
# Token ID.
attr_reader :id
attr_accessor :id
# @return [Integer, nil]
# Line number where the token was defined in the input grammar.
attr_reader :line_number
# @return [Regex::NFA, nil]
# Regex NFA for matching the token.
attr_reader :nfa
# Construct a Token.
#
# @param options [Hash]
# Optional parameters.
# @option options [String, nil] :name
# @param name [String, nil]
# Token name.
# @option options [String, nil] :pattern
# Token pattern.
# @option options [Integer, nil] :id
# Token ID.
# @option options [Integer, nil] :line_number
# @param ptypename [String, nil]
# Parser value type for this token.
# @param line_number [Integer, nil]
# Line number where the token was defined in the input grammar.
def initialize(options)
@name = options[:name]
@pattern = options[:pattern]
@id = options[:id]
@line_number = options[:line_number]
unless @pattern.nil?
regex = Regex.new(@pattern)
regex.nfa.end_state.accepts = self
@nfa = regex.nfa
end
def initialize(name, ptypename, line_number)
@name = name
@ptypename = ptypename
@line_number = line_number
end
def c_name
@name.upcase
end
# Whether the token is a drop token.
# Name of the token to use in code (special characters replaced).
#
# @return [Boolean]
# Whether the token is a drop token.
def drop?
@name.nil?
# @return [String]
# Name of the token to use in code (special characters replaced).
def code_name
self.class.code_name(@name)
end
def to_s
+17
View File
@@ -0,0 +1,17 @@
class Propane
# Utility methods.
module Util
class << self
def banner(message)
s = "*" * (message.size + 4)
"#{s}\n* #{message} *\n#{s}\n"
end
end
end
end
+68
View File
@@ -0,0 +1,68 @@
<html>
<head>
<title>Propane User Guide<%= subpage_title %> - Version <%= Propane::VERSION %></title>
<style>
body {
background-color: #CCC;
margin: 0px;
padding: 0px;
}
#body-content {
margin-left: auto;
margin-right: auto;
margin-top: 1em;
margin-bottom: 1em;
border: 1px solid black;
background-color: #FFF;
width: 120ex;
padding: 2ex;
}
a {
text-decoration: none;
}
.separator {
height: 2em;
}
.img_block_center {
display: block;
margin-left: auto;
margin-right: auto;
}
.page_nav {
width: 100%;
table-layout: fixed;
}
.page_nav_toc {
text-align: center;
}
.page_nav_next {
text-align: right;
}
.code {
padding-left: 2ex;
width: 116ex;
overflow-x: auto;
}
.ruby_code .normal {}
.ruby_code .comment { color: #005; font-style: italic; }
.ruby_code .keyword { color: #A00; font-weight: bold; }
.ruby_code .method { color: #077; }
.ruby_code .class { color: #074; }
.ruby_code .module { color: #050; }
.ruby_code .punct { color: #447; font-weight: bold; }
.ruby_code .symbol { color: #099; }
.ruby_code .string { color: #090; }
.ruby_code .char { color: #F07; }
.ruby_code .ident { color: #004; }
.ruby_code .constant { color: #07F; }
.ruby_code .regex { color: #B66; }
.ruby_code .number { color: #D55; }
.ruby_code .attribute { color: #377; }
.ruby_code .global { color: #3B7; }
.ruby_code .expr { color: #227; }
</style>
</head>
<body>
<div id="body-content"><%= content %></div>
</body>
</html>
+106
View File
@@ -0,0 +1,106 @@
#!/usr/bin/env ruby
require "fileutils"
require "digest/md5"
if File.read("lib/propane/version.rb") =~ /VERSION = "(.+)"/
VERSION = $1
else
raise "Could not determine version."
end
PROG_NAME = "propane"
START_FILE = "bin/#{PROG_NAME}"
LIB_DIR = "lib"
DIST = "dist"
files_processed = {}
combined_file = []
combine_files = lambda do |file|
File.read(file, mode: "rb").each_line do |line|
if line =~ /^\s*require(?:_relative)?\s*"(.*)"$/
require_name = $1
if require_name =~ %r{^#{PROG_NAME}(?:/.*)?$}
path = "#{LIB_DIR}/#{require_name}.rb"
if File.exist?(path)
unless files_processed[path]
files_processed[path] = true
combine_files[path]
end
else
raise "require path #{path.inspect} not found"
end
else
combined_file << line
end
else
combined_file << line
end
end
end
combine_files[START_FILE]
# Strip Ruby comment lines and empty lines to save some space, but do not
# remove lines that are in heredoc sections. This isn't terribly robust to be
# used in the wild, but works for the heredoc instances for this project.
stripped = []
heredoc_end = nil
combined_file.each do |line|
if line =~ /<<-?([A-Z]+)/
heredoc_end = $1
end
if heredoc_end and line =~ /^\s*#{heredoc_end}/
heredoc_end = nil
end
if line !~ /#\sspecs/
if heredoc_end or line !~ /^\s*(#[^!].*)?$/
stripped << line
end
end
end
license = File.read("LICENSE.txt").gsub(/^(.*?)$/) do |line|
if line.size > 0
"# #{line}"
else
"#"
end
end
require "zlib"
require "base64"
compressed_script = Zlib::Deflate.deflate(stripped.join)
encoded_compressed_script = Base64.encode64(compressed_script).gsub("\n", "")
hash = Digest::MD5.hexdigest(encoded_compressed_script)
FileUtils.rm_rf(DIST)
FileUtils.mkdir_p(DIST)
File.open("#{DIST}/#{PROG_NAME}", "wb", 0755) do |fh|
fh.write(<<EOF)
#!/usr/bin/env ruby
#{license}
script = File.join(File.dirname(__FILE__), ".propane-#{VERSION}-#{hash}.rb")
unless File.exist?(script)
if File.read(__FILE__, mode: "rb") =~ /^#==>(.*)/
require "zlib"
require "base64"
encoded_compressed = $1
unescaped_compressed = Base64.decode64(encoded_compressed)
inflated = Zlib::Inflate.inflate(unescaped_compressed)
File.open(script, "wb") do |fh|
fh.write(inflated)
end
else
raise "Could not decompress."
end
end
load script
if __FILE__ == $0
Propane::CLI.new.run(ARGV)
end
#==>#{encoded_compressed_script}
EOF
end
+227
View File
@@ -0,0 +1,227 @@
#!/usr/bin/env ruby
require "erb"
require "fileutils"
require "redcarpet"
require "syntax"
require "syntax/convertors/html"
require "propane/version"
def load_file(file_name)
contents = File.read(file_name)
contents.gsub(/\$\{include (.*?)\}/) do |match|
include_file_name = $1
File.read(include_file_name)
end
end
class Generator
class Section
attr_reader :number
attr_reader :title
attr_reader :page
attr_reader :contents
attr_reader :anchor
def initialize(number, title, page, anchor)
@number = number
@title = title.gsub(/[`]/, "")
@page = page
@anchor = anchor
@contents = ""
end
def append(contents)
@contents += contents
end
end
class Page
attr_reader :name
attr_reader :title
attr_accessor :contents
def initialize(name, title, contents = "")
@name = name
@title = title
@contents = contents
end
end
def initialize(input, output_file, multi_page)
current_page =
if multi_page
nil
else
File.basename(output_file).sub(/\.html$/, "")
end
@sections = []
@current_section_number = [0]
@lines = input.lines
while @lines.size > 0
line = @lines.slice!(0)
if line =~ /^```(.*)$/
@sections.last.append(gather_code_section($1))
elsif line.chomp == "${remove}"
remove_section
elsif line =~ /^(#+)(>)?\s*(.*)$/
level_text, new_page_text, title_text = $1, $2, $3
level = $1.size
new_page = !new_page_text.nil?
section_number = get_next_section_number(level)
anchor = make_anchor(section_number, title_text)
if multi_page and (new_page or current_page.nil?)
current_page = anchor
end
@sections << Section.new(section_number, title_text, current_page, anchor)
@sections.last.append("#{level_text} #{section_number} #{title_text}")
elsif @sections.size > 0
@sections.last.append(line)
end
end
renderer = Redcarpet::Render::HTML.new
@markdown_renderer = Redcarpet::Markdown.new(renderer)
changelog = @markdown_renderer.render(File.read("CHANGELOG.md"))
@pages = [Page.new("toc", "Table of Contents", render_toc)]
@sections.each do |section|
unless @pages.last.name == section.page
@pages << Page.new(section.page, "#{section.number} #{section.title}")
end
@pages.last.contents += render_section(section)
end
@pages.each do |page|
page.contents.gsub!("${changelog}", changelog)
page.contents.gsub!(%r{\$\{#(.+?)\}}) do |match|
section_name = $1
href = get_link_to_section(section_name)
%[<a href="#{href}">#{section_name}</a>]
end
end
template = File.read("rb/assets/user_guide.html.erb")
erb = ERB.new(template, trim_mode: "<>")
if multi_page
@pages.each_with_index do |page, page_index|
subpage_title = " - #{page.title}"
page_nav_bar = render_page_nav_bar(page_index)
content = page_nav_bar + separator + page.contents + separator + page_nav_bar
html_result = erb.result(binding.clone)
File.open(File.join(output_file, "#{page.name}.html"), "w") do |fh|
fh.write(html_result)
end
end
else
subpage_title = ""
content = @pages.reduce("") do |result, page|
result + page.contents
end
html_result = erb.result(binding.clone)
File.open(output_file, "w") do |fh|
fh.write(html_result)
end
end
end
def separator
%[<div class="separator"></div>]
end
def render_page_nav_bar(page_index)
page_nav_prev =
if page_index > 1
%[<a href="#{@pages[page_index - 1].name}.html">&laquo; Prev<br/>#{@pages[page_index - 1].title}</a>]
else
""
end
page_nav_toc =
if page_index > 0
%[<a href="toc.html">Table of Contents</a>]
else
""
end
page_nav_next =
if page_index < @pages.size - 1
%[<a href="#{@pages[page_index + 1].name}.html">Next &raquo<br/>#{@pages[page_index + 1].title}</a>]
else
""
end
%[<table class="page_nav"><tr>] + \
%[<td class="page_nav_prev">#{page_nav_prev}</td>] + \
%[<td class="page_nav_toc">#{page_nav_toc}</td>] + \
%[<td class="page_nav_next">#{page_nav_next}</td>] + \
%[</tr></table>]
end
def render_toc
toc_content = %[<h1>Table of Contents</h1>\n]
@sections.each do |section|
indent = section.number.split(".").size - 1
toc_content += %[<span style="padding-left: #{4 * indent}ex;">]
toc_content += %[<a href="#{section.page}.html##{section.anchor}">#{section.number} #{section.title}</a><br/>\n]
toc_content += %[</span>]
end
toc_content
end
def render_section(section)
%[<a name="#{section.anchor}" />] + \
@markdown_renderer.render(section.contents)
end
def make_anchor(section_number, section_title)
"s" + ("#{section_number} #{section_title}").gsub(/[^a-zA-Z0-9]/, "_")
end
def get_link_to_section(section_name)
section = @sections.find do |section|
section.title == section_name
end
raise "Could not find section #{section_name}" unless section
"#{section.page}.html##{section.anchor}"
end
def gather_code_section(syntax)
code = ""
loop do
line = @lines.slice!(0)
if line =~ /^```/
break
end
code += line
end
if syntax != ""
convertor = Syntax::Convertors::HTML.for_syntax(syntax)
%[<div class="code #{syntax}_code">\n#{convertor.convert(code)}\n</div>\n]
else
%[<div class="code">\n<pre>#{code}</pre>\n</div>\n]
end
end
def remove_section
loop do
line = @lines.slice!(0)
if line.chomp == "${/remove}"
break
end
end
end
def get_next_section_number(level)
if @current_section_number.size == level - 1
@current_section_number << 1
elsif @current_section_number.size >= level
@current_section_number[level - 1] += 1
@current_section_number.slice!(level, @current_section_number.size)
else
raise "Section level change from #{@current_section_number.size} to #{level}"
end
@current_section_number.join(".")
end
end
input = load_file("doc/user_guide.md")
FileUtils.rm_rf("gen/user_guide")
FileUtils.mkdir_p("gen/user_guide")
Generator.new(input, "gen/user_guide/user_guide.html", false)
Generator.new(input, "gen/user_guide", true)
+179
View File
@@ -0,0 +1,179 @@
<<
import std.math;
import json_types;
private string string_value;
>>
ptype JSONValue;
ptype array = JSONValue[];
ptype dict = JSONValue[string];
ptype string = string;
drop /\s+/;
token lbrace /\{/;
token rbrace /\}/;
token lbracket /\[/;
token rbracket /\]/;
token comma /,/;
token colon /:/;
token number /-?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][-+]?[0-9]+)?/ <<
double n;
bool negative;
size_t i = 0u;
if (match[i] == '-')
{
negative = true;
i++;
}
while ('0' <= match[i] && match[i] <= '9')
{
n *= 10.0;
n += (match[i] - '0');
i++;
}
if (match[i] == '.')
{
i++;
double mult = 0.1;
while ('0' <= match[i] && match[i] <= '9')
{
n += mult * (match[i] - '0');
mult /= 10.0;
i++;
}
}
if (match[i] == 'e' || match[i] == 'E')
{
bool exp_negative;
i++;
if (match[i] == '-')
{
exp_negative = true;
i++;
}
else if (match[i] == '+')
{
i++;
}
long exp;
while ('0' <= match[i] && match[i] <= '9')
{
exp *= 10;
exp += (match[i] - '0');
i++;
}
if (exp_negative)
{
exp = -exp;
}
n = pow(n, exp);
}
if (negative)
{
n = -n;
}
$$ = new JSONNumber(n);
>>
token true <<
$$ = new JSONTrue();
>>
token false <<
$$ = new JSONFalse();
>>
token null <<
$$ = new JSONNull();
>>
/"/ <<
$mode(string);
string_value = "";
>>
string: token string (string) /"/ <<
$$ = string_value;
$mode(default);
>>
string: /\\"/ <<
string_value ~= "\"";
>>
string: /\\\\/ <<
string_value ~= "\\";
>>
string: /\\\// <<
string_value ~= "/";
>>
string: /\\b/ <<
string_value ~= "\b";
>>
string: /\\f/ <<
string_value ~= "\f";
>>
string: /\\n/ <<
string_value ~= "\n";
>>
string: /\\r/ <<
string_value ~= "\r";
>>
string: /\\t/ <<
string_value ~= "\t";
>>
string: /\\u[0-9a-fA-F]{4}/ <<
/* Not actually going to encode the code point for this example... */
string_value ~= "{" ~ match[2..6] ~ "}";
>>
string: /[^\\]/ <<
string_value ~= match;
>>
Start -> Value <<
$$ = $1;
>>
Value -> string <<
$$ = new JSONString($1);
>>
Value -> number <<
$$ = $1;
>>
Value -> Object <<
$$ = $1;
>>
Value -> Array <<
$$ = $1;
>>
Value -> true <<
$$ = $1;
>>
Value -> false <<
$$ = $1;
>>
Value -> null <<
$$ = $1;
>>
Object -> lbrace rbrace <<
$$ = new JSONObject();
>>
Object -> lbrace KeyValues rbrace <<
$$ = new JSONObject($2);
>>
KeyValues (dict) -> KeyValue <<
$$ = $1;
>>
KeyValues -> KeyValues comma KeyValue <<
foreach (key, value; $3)
{
$1[key] = value;
}
$$ = $1;
>>
KeyValue (dict) -> string colon Value <<
$$ = [$1: $3];
>>
Array -> lbracket rbracket <<
$$ = new JSONArray();
>>
Array -> lbracket Values rbracket <<
$$ = new JSONArray($2);
>>
Values (array) -> Value <<
$$ = [$1];
>>
Values -> Values comma Value <<
$$ = $1 ~ [$3];
>>
+63
View File
@@ -0,0 +1,63 @@
class JSONValue
{
}
class JSONObject : JSONValue
{
JSONValue[string] value;
this()
{
}
this(JSONValue[string] value)
{
this.value = value;
}
}
class JSONArray : JSONValue
{
JSONValue[] value;
this()
{
}
this(JSONValue[] value)
{
this.value = value;
}
}
class JSONNumber : JSONValue
{
double value;
this(double value)
{
this.value = value;
}
}
class JSONString : JSONValue
{
string value;
this(string value)
{
this.value = value;
}
}
class JSONTrue : JSONValue
{
}
class JSONFalse : JSONValue
{
}
class JSONNull : JSONValue
{
}
+229
View File
@@ -0,0 +1,229 @@
class Propane
describe Grammar do
it "parses a user grammar" do
input = <<EOF
# Comment line
module a.b;
class Foobar;
ptype XYZ * ;
token while;
token id
/[a-zA-Z_][a-zA-Z_0-9]*/;
token token_with_code <<
Code for the token
>>
tokenid token_with_no_pattern;
drop /\\s+/;
A -> B <<
a = 42;
>>
B -> C while id;
B -> <<
b = 0;
>>
EOF
grammar = Grammar.new(input)
expect(grammar.classname).to eq "Foobar"
expect(grammar.modulename).to eq "a.b"
expect(grammar.ptype).to eq "XYZ *"
expect(grammar.ptypes).to eq("default" => "XYZ *")
o = grammar.tokens.find {|token| token.name == "while"}
expect(o).to_not be_nil
expect(o.line_number).to eq 7
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.pattern).to eq "while"
expect(o.line_number).to eq 7
expect(o.code).to be_nil
o = grammar.tokens.find {|token| token.name == "id"}
expect(o).to_not be_nil
expect(o.line_number).to eq 10
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.pattern).to eq "[a-zA-Z_][a-zA-Z_0-9]*"
expect(o.line_number).to eq 10
expect(o.code).to be_nil
o = grammar.tokens.find {|token| token.name == "token_with_code"}
expect(o).to_not be_nil
expect(o.line_number).to eq 12
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.pattern).to eq "token_with_code"
expect(o.line_number).to eq 12
expect(o.code).to eq "Code for the token\n"
o = grammar.tokens.find {|token| token.name == "token_with_no_pattern"}
expect(o).to_not be_nil
expect(o.line_number).to eq 16
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to be_nil
o = grammar.patterns.find {|pattern| pattern.pattern == "\\s+"}
expect(o).to_not be_nil
expect(o.line_number).to eq 18
expect(o.token).to be_nil
expect(o.code).to be_nil
expect(grammar.rules.size).to eq 3
o = grammar.rules[0]
expect(o.name).to eq "A"
expect(o.components).to eq %w[B]
expect(o.line_number).to eq 20
expect(o.code).to eq " a = 42;\n"
o = grammar.rules[1]
expect(o.name).to eq "B"
expect(o.components).to eq %w[C while id]
expect(o.line_number).to eq 23
expect(o.code).to be_nil
o = grammar.rules[2]
expect(o.name).to eq "B"
expect(o.components).to eq []
expect(o.line_number).to eq 24
expect(o.code).to eq " b = 0;\n"
end
it "parses code segments with semicolons" do
input = <<EOF
token code1 <<
a = b;
return c;
>>
token code2 <<
writeln("Hello there");
>>
tokenid token_with_no_pattern;
EOF
grammar = Grammar.new(input)
o = grammar.tokens.find {|token| token.name == "code1"}
expect(o).to_not be_nil
expect(o.line_number).to eq 1
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"
o = grammar.tokens.find {|token| token.name == "code2"}
expect(o).to_not be_nil
expect(o.line_number).to eq 6
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.code).to eq %[ writeln("Hello there");\n]
end
it "supports mode labels" do
input = <<EOF
token a;
m1: token b;
/foo/ <<
>>
m2: /bar/ <<
>>
drop /q/;
m3: drop /r/;
EOF
grammar = Grammar.new(input)
o = grammar.tokens.find {|token| token.name == "a"}
expect(o).to_not be_nil
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.mode).to be_nil
o = grammar.tokens.find {|token| token.name == "b"}
expect(o).to_not be_nil
o = grammar.patterns.find {|pattern| pattern.token == o}
expect(o).to_not be_nil
expect(o.mode).to eq "m1"
o = grammar.patterns.find {|pattern| pattern.pattern == "foo"}
expect(o).to_not be_nil
expect(o.mode).to be_nil
o = grammar.patterns.find {|pattern| pattern.pattern == "bar"}
expect(o).to_not be_nil
expect(o.mode).to eq "m2"
o = grammar.patterns.find {|pattern| pattern.pattern == "q"}
expect(o).to_not be_nil
expect(o.mode).to be_nil
o = grammar.patterns.find {|pattern| pattern.pattern == "r"}
expect(o).to_not be_nil
expect(o.mode).to eq "m3"
end
it "allows assigning ptypes to tokens and rules" do
input = <<EOF
ptype Subnode *;
ptype string = char *;
ptype integer = int;
ptype node = Node *;
token abc(string);
token bar;
tokenid int(integer);
/xyz/ (string) <<
>>
/z28/ <<
>>
Start (node) -> R;
R -> abc int;
EOF
grammar = Grammar.new(input)
o = grammar.tokens.find {|token| token.name == "abc"}
expect(o).to_not be_nil
expect(o.ptypename).to eq "string"
o = grammar.tokens.find {|token| token.name == "bar"}
expect(o).to_not be_nil
expect(o.ptypename).to be_nil
o = grammar.tokens.find {|token| token.name == "int"}
expect(o).to_not be_nil
expect(o.ptypename).to eq "integer"
o = grammar.rules.find {|rule| rule.name == "Start"}
expect(o).to_not be_nil
expect(o.ptypename).to eq "node"
o = grammar.rules.find {|rule| rule.name == "R"}
expect(o).to_not be_nil
expect(o.ptypename).to be_nil
o = grammar.patterns.find {|pattern| pattern.pattern == "xyz"}
expect(o).to_not be_nil
expect(o.ptypename).to eq "string"
o = grammar.patterns.find {|pattern| pattern.pattern == "z28"}
expect(o).to_not be_nil
expect(o.ptypename).to be_nil
end
end
end
+26 -16
View File
@@ -35,7 +35,8 @@ class TestLexer
end
end
if last_accepts
[last_accepts.name, last_s]
name = last_accepts.token ? last_accepts.token.name : nil
[name, last_s]
end
end
@@ -51,7 +52,7 @@ end
def run(grammar, input)
grammar = Propane::Grammar.new(grammar)
token_dfa = Propane::Lexer::DFA.new(grammar.tokens, grammar.drop_tokens)
token_dfa = Propane::Lexer::DFA.new(grammar.patterns)
test_lexer = TestLexer.new(token_dfa)
test_lexer.lex(input)
end
@@ -59,7 +60,7 @@ end
describe Propane::Lexer::DFA do
it "lexes a simple token" do
expect(run(<<EOF, "foo")).to eq [["foo", "foo"]]
token foo
token foo;
EOF
end
@@ -69,8 +70,8 @@ EOF
["bar", "bar"],
]
expect(run(<<EOF, "foobar")).to eq expected
token foo
token bar
token foo;
token bar;
EOF
end
@@ -79,17 +80,17 @@ EOF
["identifier", "foobar"],
]
expect(run(<<EOF, "foobar")).to eq expected
token foo
token bar
token identifier [a-z]+
token foo;
token bar;
token identifier /[a-z]+/;
EOF
expected = [
["plusplus", "++"],
["plus", "+"],
]
expect(run(<<EOF, "+++")).to eq expected
token plus \\+
token plusplus \\+\\+
token plus /\\+/;
token plusplus /\\+\\+/;
EOF
end
@@ -100,9 +101,9 @@ EOF
["bar", "bar"],
]
expect(run(<<EOF, "foo \tbar")).to eq expected
token foo
token bar
token WS \\s+
token foo;
token bar;
token WS /\\s+/;
EOF
end
@@ -113,9 +114,18 @@ EOF
["bar", "bar"],
]
expect(run(<<EOF, "foo \tbar")).to eq expected
token foo
token bar
drop \\s+
token foo;
token bar;
drop /\\s+/;
EOF
end
it "matches a semicolon" do
expected = [
["semicolon", ";"],
]
expect(run(<<EOF, ";")).to eq expected
token semicolon /;/;
EOF
end
end
+281 -20
View File
@@ -1,23 +1,60 @@
require "fileutils"
require "open3"
Results = Struct.new(:stdout, :stderr, :status)
describe Propane do
def write_grammar(grammar)
File.write("spec/run/testparser.propane", grammar)
end
def build_parser
result = system(*%w[./propane.sh spec/run/testparser.propane spec/run/testparser.d])
expect(result).to be_truthy
def build_parser(options = {})
command = %w[./propane.sh spec/run/testparser.propane spec/run/testparser.d --log spec/run/testparser.log]
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_file)
result = system(*%w[gdc -funittest -o spec/run/testparser spec/run/testparser.d], test_file)
def compile(*test_files)
result = system(*%w[ldc2 --unittest -of spec/run/testparser spec/run/testparser.d -Ispec], *test_files)
expect(result).to be_truthy
end
def run
result = system("spec/run/testparser")
expect(result).to be_truthy
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.new(stdout, stderr, status)
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
@@ -27,10 +64,10 @@ describe Propane do
it "generates a D lexer" do
write_grammar <<EOF
token int \\d+
token plus \\+
token times \\*
drop \\s+
token int /\\d+/;
token plus /\\+/;
token times /\\*/;
drop /\\s+/;
Start -> Foo;
Foo -> int <<
>>
@@ -39,15 +76,17 @@ Foo -> plus <<
EOF
build_parser
compile("spec/test_d_lexer.d")
run
results = run
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
token plus /\\+/;
token times /\\*/;
token zero /0/;
token one /1/;
Start -> E;
E -> E times B;
E -> E plus B;
@@ -58,26 +97,248 @@ EOF
build_parser
end
it "generates an SLR parser" do
write_grammar <<EOF
token one /1/;
Start -> E;
E -> one E;
E -> one;
EOF
build_parser
end
it "distinguishes between multiple identical rules with lookahead symbol" do
write_grammar <<EOF
token a
token b
token a;
token b;
Start -> R1 a;
Start -> R2 b;
R1 -> a b;
R2 -> a b;
EOF
build_parser
compile("spec/test_d_parser_identical_rules_lookahead.d")
results = run
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
token a;
token b;
drop /\\s+/;
Start -> a R1;
Start -> b R1;
R1 -> b;
EOF
build_parser
compile("spec/test_d_parser_rule_from_multiple_states.d")
results = run
expect(results.status).to eq 0
end
it "executes user code when matching lexer token" do
write_grammar <<EOF
token abc <<
writeln("abc!");
>>
token def;
Start -> Abcs def;
Abcs -> ;
Abcs -> abc Abcs;
EOF
build_parser
compile("spec/test_user_code.d")
results = run
expect(results.status).to eq 0
verify_lines(results.stdout, [
"abc!",
"pass1",
"abc!",
"abc!",
"pass2",
])
end
it "supports a pattern statement" do
write_grammar <<EOF
token abc;
/def/ <<
writeln("def!");
>>
Start -> abc;
EOF
build_parser
compile("spec/test_pattern.d")
results = run
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
write_grammar <<EOF
token abc;
/def/ <<
writeln("def!");
>>
/ghi/ <<
writeln("ghi!");
return $token(abc);
>>
Start -> abc;
EOF
build_parser
compile("spec/test_return_token_from_pattern.d")
results = run
expect(results.status).to eq 0
verify_lines(results.stdout, [
"def!",
"ghi!",
"def!",
])
end
it "supports lexer modes" do
write_grammar <<EOF
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
build_parser
compile("spec/test_lexer_modes.d")
results = run
expect(results.status).to eq 0
verify_lines(results.stdout, [
"begin string mode",
"captured string",
"pass1",
"begin string mode",
"captured string",
"pass2",
])
end
it "executes user code associated with a parser rule" do
write_grammar <<EOF
token a;
token b;
Start -> A B <<
writeln("Start!");
>>
A -> a <<
writeln("A!");
>>
B -> b <<
writeln("B!");
>>
EOF
build_parser
compile("spec/test_parser_rule_user_code.d")
results = run
expect(results.status).to eq 0
verify_lines(results.stdout, [
"A!",
"B!",
"Start!",
])
end
it "parses lists" do
write_grammar <<EOF
ptype uint;
token a;
Start -> As <<
$$ = $1;
>>
As -> <<
$$ = 0u;
>>
As -> As a <<
$$ = $1 + 1u;
>>
EOF
build_parser
compile("spec/test_parsing_lists.d")
results = run
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 = build_parser(capture: true)
expect(results.status).to_not eq 0
expect(results.stderr).to match %r{reduce/reduce conflict.*\(E\).*\(F\)}
end
it "provides matched text to user code blocks" do
write_grammar <<EOF
token id /[a-zA-Z_][a-zA-Z0-9_]*/ <<
writeln("Matched token is ", match);
>>
Start -> id;
EOF
build_parser
compile("spec/test_lexer_match_text.d")
results = run
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
write_grammar <<EOF
ptype ulong;
token word /[a-z]+/ <<
$$ = match.length;
>>
Start -> word <<
$$ = $1;
>>
EOF
build_parser
compile("spec/test_lexer_result_value.d")
results = run
expect(results.stderr).to eq ""
expect(results.status).to eq 0
end
it "allows creating a JSON parser" do
write_grammar(File.read("spec/json_parser.propane"))
build_parser
compile("spec/test_parsing_json.d", "spec/json_types.d")
end
end
+31 -63
View File
@@ -8,77 +8,45 @@ int main()
unittest
{
alias DCP = Testparser.Decoder.DecodedCodePoint;
string inputstring = "5+\n 66";
const(ubyte) * input = cast(const(ubyte) *)inputstring.ptr;
size_t input_length = inputstring.length;
DCP dcp;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP('5', 1u));
input += dcp.code_point_length;
input_length -= dcp.code_point_length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP('+', 1u));
input += dcp.code_point_length;
input_length -= dcp.code_point_length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP('\n', 1u));
input += dcp.code_point_length;
input_length -= dcp.code_point_length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP(' ', 1u));
input += dcp.code_point_length;
input_length -= dcp.code_point_length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP('6', 1u));
input += dcp.code_point_length;
input_length -= dcp.code_point_length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP('6', 1u));
input += dcp.code_point_length;
input_length -= dcp.code_point_length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP(Testparser.Decoder.CODE_POINT_EOF, 0u));
alias Result = Testparser.Decoder.Result;
Result result;
inputstring = "\xf0\x1f\x27\x21";
input = cast(const(ubyte) *)inputstring.ptr;
input_length = inputstring.length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP(0x1F9E1, 4u));
result = Testparser.Decoder.decode_code_point("5");
assert(result == Result.success('5', 1u));
inputstring = "\xf0\x1f\x27";
input = cast(const(ubyte) *)inputstring.ptr;
input_length = inputstring.length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP(Testparser.Decoder.CODE_POINT_INVALID, 0u));
result = Testparser.Decoder.decode_code_point("");
assert(result == Result.eof());
inputstring = "\xf0\x1f\x27\xFF";
input = cast(const(ubyte) *)inputstring.ptr;
input_length = inputstring.length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP(Testparser.Decoder.CODE_POINT_INVALID, 0u));
result = Testparser.Decoder.decode_code_point("\xC2\xA9");
assert(result == Result.success(0xA9u, 2u));
inputstring = "\xfe";
input = cast(const(ubyte) *)inputstring.ptr;
input_length = inputstring.length;
dcp = Testparser.Decoder.decode_code_point(input, input_length);
assert(dcp == DCP(Testparser.Decoder.CODE_POINT_INVALID, 0u));
result = Testparser.Decoder.decode_code_point("\xf0\x9f\xa7\xa1");
assert(result == Result.success(0x1F9E1, 4u));
result = Testparser.Decoder.decode_code_point("\xf0\x9f\x27");
assert(result == Result.decode_error());
result = Testparser.Decoder.decode_code_point("\xf0\x9f\xa7\xFF");
assert(result == Result.decode_error());
result = Testparser.Decoder.decode_code_point("\xfe");
assert(result == Result.decode_error());
}
unittest
{
alias LT = Testparser.Lexer.LexedToken;
alias Result = Testparser.Lexer.Result;
string input = "5 + 4 * \n677 + 567";
Testparser.Lexer lexer = new Testparser.Lexer(cast(const(ubyte) *)input.ptr, input.length);
assert(lexer.lex_token() == LT(0, 0, 1, Testparser.TOKEN_INT));
assert(lexer.lex_token() == LT(0, 2, 1, Testparser.TOKEN_PLUS));
assert(lexer.lex_token() == LT(0, 4, 1, Testparser.TOKEN_INT));
assert(lexer.lex_token() == LT(0, 6, 1, Testparser.TOKEN_TIMES));
assert(lexer.lex_token() == LT(1, 0, 3, Testparser.TOKEN_INT));
assert(lexer.lex_token() == LT(1, 4, 1, Testparser.TOKEN_PLUS));
assert(lexer.lex_token() == LT(1, 6, 3, Testparser.TOKEN_INT));
assert(lexer.lex_token() == LT(1, 9, 0, Testparser.TOKEN_EOF));
Testparser.Lexer lexer = new Testparser.Lexer(input);
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 0, 0, 1, Testparser.TOKEN_int));
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 0, 2, 1, Testparser.TOKEN_plus));
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 0, 4, 1, Testparser.TOKEN_int));
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 0, 6, 1, Testparser.TOKEN_times));
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 1, 0, 3, Testparser.TOKEN_int));
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 1, 4, 1, Testparser.TOKEN_plus));
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 1, 6, 3, Testparser.TOKEN_int));
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 1, 9, 0, Testparser.TOKEN___EOF));
lexer = new Testparser.Lexer(null, 0u);
assert(lexer.lex_token() == LT(0, 0, 0, Testparser.TOKEN_EOF));
lexer = new Testparser.Lexer("");
assert(lexer.lex_token() == Result(Result.Type.TOKEN, 0, 0, 0, Testparser.TOKEN___EOF));
}
@@ -0,0 +1,18 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = "aba";
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
input = "abb";
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
}
@@ -0,0 +1,22 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = "a";
auto parser = new Testparser.Parser(input);
assert(parser.parse() == false);
input = "a b";
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
input = "bb";
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
}
+15
View File
@@ -0,0 +1,15 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = `identifier_123`;
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
writeln("pass1");
}
+20
View File
@@ -0,0 +1,20 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = `abc "a string" def`;
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
writeln("pass1");
input = `abc "abc def" def`;
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
writeln("pass2");
}
+20
View File
@@ -0,0 +1,20 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = `x`;
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
assert(parser.result == 1u);
input = `fabulous`;
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
assert(parser.result == 8u);
}
+14
View File
@@ -0,0 +1,14 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = "ab";
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
}
+55
View File
@@ -0,0 +1,55 @@
import testparser;
import std.stdio;
import json_types;
int main()
{
return 0;
}
unittest
{
string input = ``;
auto parser = new Testparser.Parser(input);
assert(parser.parse());
input = `{}`;
parser = new Testparser.Parser(input);
assert(parser.parse());
assert(cast(JSONObject)parser.result);
input = `[]`;
parser = new Testparser.Parser(input);
assert(parser.parse());
assert(cast(JSONArray)parser.result);
input = `-45.6`;
parser = new Testparser.Parser(input);
assert(parser.parse());
assert(cast(JSONNumber)parser.result);
assert((cast(JSONNumber)parser.result).value == -45.6);
input = `2E-2`;
parser = new Testparser.Parser(input);
assert(parser.parse());
assert(cast(JSONNumber)parser.result);
assert((cast(JSONNumber)parser.result).value == 0.02);
input = `{"hi":true}`;
parser = new Testparser.Parser(input);
assert(parser.parse());
assert(cast(JSONObject)parser.result);
JSONObject o = cast(JSONObject)parser.result;
assert(o.value["hi"]);
assert(cast(JSONTrue)o.value["hi"]);
input = `{"ff": false, "nn": null}`;
parser = new Testparser.Parser(input);
assert(parser.parse());
assert(cast(JSONObject)parser.result);
o = cast(JSONObject)parser.result;
assert(o.value["ff"]);
assert(cast(JSONFalse)o.value["ff"]);
assert(o.value["nn"]);
assert(cast(JSONNull)o.value["nn"]);
}
+25
View File
@@ -0,0 +1,25 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = "a";
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
assert(parser.result == 1u);
input = "";
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
assert(parser.result == 0u);
input = "aaaaaaaaaaaaaaaa";
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
assert(parser.result == 16u);
}
+20
View File
@@ -0,0 +1,20 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = "abcdef";
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
writeln("pass1");
input = "defabcdef";
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
writeln("pass2");
}
+14
View File
@@ -0,0 +1,14 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = "defghidef";
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
}
+20
View File
@@ -0,0 +1,20 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
string input = "abcdef";
auto parser = new Testparser.Parser(input);
assert(parser.parse() == true);
writeln("pass1");
input = "abcabcdef";
parser = new Testparser.Parser(input);
assert(parser.parse() == true);
writeln("pass2");
}