Compare commits

..
239 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
josh a91bf2f250 Clean up parser ItemSet processing 2022-06-14 22:57:07 -04:00
josh 0f1c00d1a6 Add RuleSet#could_be_empty? 2022-06-14 07:35:58 -04:00
josh 291509f005 Add Rule#empty? 2022-06-14 07:35:38 -04:00
josh c1769503a8 Add Item#complete? 2022-06-14 07:35:07 -04:00
josh 8b152abaa7 Add ItemSet#leading_item_sets 2022-06-14 07:34:35 -04:00
josh d6e5c4325d Add new start rule ending with EOF token rather than appending EOF token to all user-specified start rules 2022-06-07 22:25:31 -04:00
josh 2837dfda6b Add Rule#id 2022-06-06 22:30:53 -04:00
josh 9d850294a9 Clean up and comment Item and ItemSet 2022-06-05 18:12:14 -04:00
josh df8088c3c6 Clean up rule format in grammar files 2022-06-05 16:28:35 -04:00
josh ba74d0a20a Reduce maximum code point value to not interfere with magic code point values used by parser 2022-06-05 15:24:40 -04:00
josh fe607291f4 Use .propane extension for test grammars 2022-06-05 15:18:55 -04:00
josh c42c3576a5 Remove RuleSet creation from Grammar 2022-06-05 15:16:41 -04:00
josh ca6a93a4c5 Move generation logic to new Generator class 2022-06-05 14:57:58 -04:00
josh b1d5cddab4 Change Token constructor to take a Hash of all parameters 2022-06-05 14:40:41 -04:00
josh f37801ec9e Store tokens and drop tokens separately 2022-06-05 14:36:19 -04:00
josh 6f1ce32775 Move Lexer::DFA#build_tables to Lexer 2022-06-05 14:18:35 -04:00
josh 57a3e9d9f6 Move grammar parsing into new Grammar class 2022-06-05 10:23:34 -04:00
josh 34eb1370ff Move RuleSet::Patern to top-level Rule class 2022-06-04 17:50:26 -04:00
josh 7598c589fe Detect other invalid UTF-8 encodings 2022-05-31 22:26:09 -04:00
josh a0af8b0d7c Rename Rule to RuleSet 2022-05-30 15:49:12 -04:00
josh 164a4854fb Update README 2022-05-30 15:40:31 -04:00
josh ddadc2008b Rename to propane 2022-05-28 20:20:03 -04:00
josh fbd215098b Update license years 2022-05-27 21:49:54 -04:00
josh bfe2916165 Update bundler 2022-05-27 00:15:03 -04:00
josh c9bc4832f4 bundle update 2022-05-27 00:14:26 -04:00
josh 6dfef8573f Fix ERB constructor call for Ruby 3.2 warnings 2022-05-27 00:12:40 -04:00
josh f3ed678fe1 Store tokens in Hash by name 2021-09-27 21:40:12 -04:00
josh 280b749e38 Track Rule IDs 2021-09-27 21:29:44 -04:00
josh d6779aef00 Start on Parser#build_tables 2021-09-22 23:26:36 -04:00
josh 746ec89be8 Add test for a rule that can be arrived at from multiple states 2021-09-21 21:40:11 -04:00
josh 997f34a1e4 Keep track of item set in-links 2021-09-21 21:32:18 -04:00
josh a2795bb531 Keep track of follow item sets by symbol for each item set 2021-09-21 17:09:53 -04:00
josh 850e639e3a update identical rule spec to use lookahead symbol 2021-09-06 20:18:17 -04:00
josh 5f7e548fe3 Remove Rule::Pattern, Item stores a Rule reference 2021-09-06 19:41:29 -04:00
josh bdb10e7afc test duplicate rules 2021-09-05 09:50:04 -04:00
josh 7bdaf7cdbc Do not create item set following EOF token 2021-09-05 07:51:59 -04:00
josh 08e3516ad9 Add wikipedia LR(0) parser example test 2021-09-04 22:33:34 -04:00
josh 2c8f3c6e9a Avoid infinite loop with self-referential rules 2021-09-04 22:29:10 -04:00
josh 9dffa3c41a Recursively build item sets 2021-08-29 12:38:44 -04:00
josh ceb7e9ee32 Add EOF token to Start rule patterns 2021-08-29 11:48:49 -04:00
josh 6026bf1514 Start building following item sets 2021-08-29 09:41:00 -04:00
josh 9cc1890ddc One Rule object stores all alternative patterns 2021-08-28 10:28:50 -04:00
josh e4f2fffe50 add Item#closed_items 2021-08-28 09:47:01 -04:00
josh d931bcb513 Do not expand rules 2021-08-28 09:23:08 -04:00
josh 2e16b0bd6e Start on Item and ItemSet 2021-08-28 09:02:19 -04:00
josh 6ce94e15af Expand rules 2021-08-28 08:11:06 -04:00
josh 3f92ae46c4 Map rule components to Token/Rule references 2021-08-22 21:21:41 -04:00
josh 00016f16b3 Combine Grammar and Generator into top-level Imbecile class 2021-08-22 21:04:46 -04:00
josh 9273bfccf6 Move Token/Rule out of Grammar class 2021-08-19 20:00:40 -04:00
josh f295acb593 Generator builds a Lexer, not a Lexer::DFA 2021-08-19 13:11:12 -04:00
josh 51a31317a6 Move FA#build_tables to Lexer::DFA 2021-08-19 11:55:34 -04:00
josh 9459883e74 Add Lexer class; Move LexerDFA to Lexer::DFA 2021-08-18 17:09:45 -04:00
josh 28591907c1 Move FA class out of Regex class 2021-08-18 17:05:03 -04:00
josh 37d6917b49 Rework Rule constructor 2021-07-27 21:22:46 -04:00
josh 2685c05360 Change rule syntax 2021-07-19 21:55:08 -04:00
josh c0c3353fd7 Test lexing empty null string returns EOF 2021-07-06 12:06:07 -04:00
josh 3158e51059 Add length field to LexedToken 2021-07-06 11:59:35 -04:00
josh d9e4f64d2e Fix returning TOKEN_EOF when lexing at EOF 2021-07-06 11:55:44 -04:00
josh ec2dcf9a72 Fix not progressing through input while lexing a token 2021-07-06 11:47:33 -04:00
josh 578e165e2d Fix off-by-one error in state IDs 2021-07-06 11:44:03 -04:00
josh e8df4296cc Begin testing lexer 2021-07-06 11:09:39 -04:00
josh 230c324209 Fix iterating through all transitions in a state 2021-07-06 11:09:13 -04:00
josh 1271e19b50 Test multi-byte code point decoding 2021-07-06 11:02:43 -04:00
josh 12e11399af Add decoder tests 2021-07-06 10:57:06 -04:00
josh 24fab8515d Decoder.decode_code_point returns struct with code point and length together 2021-07-06 10:50:32 -04:00
josh 1dcdd87a28 Generate token constants and names to top-level parser class 2021-07-06 10:28:35 -04:00
josh 8aec7ec0de Lexer class can be used standalone 2021-07-06 10:15:07 -04:00
josh c96d55b031 Fix class name 2021-07-06 10:14:14 -04:00
josh ca7d4862f9 Run test executable; build with unit tests 2021-07-06 10:03:42 -04:00
josh 3c874ae4c1 Compile generated parser with a test file 2021-07-05 23:05:55 -04:00
josh 748c219625 Do not return dropped tokens from Lexer.lex_token() 2021-07-05 22:53:58 -04:00
josh 71ee7de9f9 Remove obsolete lex() and lex_token() methods 2021-07-05 22:49:50 -04:00
josh 2121acc87e Complete Lexer.lex_token() 2021-07-05 22:41:09 -04:00
josh f2563cf255 Work on Lexer.lex_token() 2021-07-05 22:02:27 -04:00
josh 24d12be3b9 Add TOKEN enum entries for EOF, decode error, drop, and none 2021-07-05 20:11:55 -04:00
josh 91d6ee25ea Add Lexer class 2021-07-05 19:13:41 -04:00
josh 2f1cb47bea Add Decoder class to decode code points 2021-07-05 18:47:10 -04:00
josh 651461c570 Start on decode_code_point() 2021-06-29 23:17:44 -04:00
josh 3ce54bd303 Start on lex()/lex_token() 2021-06-29 23:10:40 -04:00
josh 15454f926a Add TokenNames array 2021-06-29 22:54:24 -04:00
josh 4beb3d2016 Add some token constants 2021-06-27 23:09:42 -04:00
josh aae7bc188c Use unsigned literals 2021-06-26 18:11:20 -04:00
josh a716dedeb6 Start on test framework to compile and run generated parser 2021-06-26 16:17:24 -04:00
josh 93cb25df62 Do not generate token names for drop tokens 2021-06-26 16:16:18 -04:00
josh 61dd5bc5a0 Move imbecile_spec to lexer_dfa_spec 2021-06-26 16:01:49 -04:00
josh 10a8ef5eb4 Update generated lexer state and transition tables 2021-06-26 15:58:36 -04:00
josh 98584ce07a Add FA#build_tables 2021-06-24 15:06:10 -04:00
josh 2122ca02fe Start generating lexer states and transitions 2021-06-23 23:15:02 -04:00
josh 5881f13380 Generate enum of token identifiers 2021-06-23 22:22:45 -04:00
josh ebc1d8f001 Fix FA#to_s to show correct destination state 2021-06-23 22:21:53 -04:00
josh 5fecd5c6a2 Refactor into FA#enumerate 2021-06-22 22:01:39 -04:00
josh 5b688b090d Add some attr_readers 2021-06-21 22:52:27 -04:00
josh f77218801f Error if Start rule not found 2021-06-21 22:48:17 -04:00
josh 70118dd019 Check for duplicate token/rule names in Generator 2021-06-21 22:34:43 -04:00
josh d552f2a540 CLI: accept --log option 2021-06-19 12:06:02 -04:00
josh d2fac07249 Add Generator class 2021-06-15 16:51:36 -04:00
josh a34272dfd6 Add Grammar::Rule class 2021-06-14 22:49:43 -04:00
josh 9d05861819 Parse grammar input by multiline regex 2021-06-12 22:57:32 -04:00
josh 03035a25a5 Update spec task to accept an example pattern 2021-06-12 22:46:13 -04:00
josh db70f8b94d Add "drop" grammar keyword to drop patterns 2021-06-09 22:48:30 -04:00
josh f67dd62b20 Add \s to expand to whitespace characters 2021-06-09 22:37:00 -04:00
josh c6bac6d3a1 Rename TokenDFA -> LexerDFA 2021-06-08 13:54:46 -04:00
josh aa92970c31 Add some lexer tests 2021-06-07 22:21:52 -04:00
josh b8282e748e Start on a test lexer for lexer specs 2021-06-07 17:17:37 -04:00
josh 930ac56148 Do not accept 0-length tokens 2021-06-06 15:29:30 -04:00
josh 7f54778ba8 Rename Regex::DFA to TokenDFA 2021-06-06 15:18:21 -04:00
josh 701903def2 Token should build its own NFA 2021-06-06 14:09:28 -04:00
josh afea886ecb Add Grammar::Token class 2021-06-06 14:04:33 -04:00
josh 03b2e87186 Grammar takes in input string instead of file name 2021-06-06 10:09:53 -04:00
josh e4370cac62 Print accepting token in FA#to_s 2021-06-06 09:59:28 -04:00
josh ed3f599e25 Create common FA/State/Transition classes across NFA/DFA 2021-06-06 09:41:23 -04:00
josh 1228a76c55 Fix MultiplicityUnit#to_nfa again 2021-05-26 10:17:03 -04:00
josh 538e360cb3 Fix MultiplicityUnit#to_nfa 2021-05-25 16:59:22 -04:00
josh e7f8c3726c Fix NFA#to_s 2021-05-25 16:14:19 -04:00
josh b6e3a5c151 Record accepting token in DFA state 2021-05-25 16:00:25 -04:00
josh 35ef94dbd3 Print out DFA to test 2021-05-25 15:52:47 -04:00
josh 37e1252ded Continue building DFA 2021-05-25 15:44:23 -04:00
josh 214ece7d90 Add NFA::Transition, start on DFA construction 2021-05-23 21:41:50 -04:00
josh 8473df421a Add specs for CodePointRange 2021-05-23 20:41:40 -04:00
josh 3987f08cd7 Add CodePointRange class 2021-05-23 17:52:20 -04:00
josh 3a1650906e Show non-printable characters better in NFA#to_s 2021-05-21 14:39:02 -04:00
josh 952bffc33c Move DFA#nil_transition_states to NFA::State 2021-05-21 14:27:42 -04:00
josh f64f3683c6 Add NFA#to_s 2021-05-21 14:24:16 -04:00
josh 43f5caf449 Fix some NFA creation 2021-05-20 17:34:18 -04:00
josh f38a7456e9 Add DFA#nil_transition_states 2021-05-20 17:08:34 -04:00
josh c77c81bf25 Mark regex NFA end state as accepting the token 2021-05-18 16:34:26 -04:00
josh 7196a0605a Add DFA class 2021-05-18 16:31:16 -04:00
josh 24054461a2 Merge Regex::Parser into Regex, move Unit to its own file 2021-05-18 16:14:42 -04:00
josh 89a5976064 Make Regex::Parser build a NFA after parsing 2021-05-18 16:07:39 -04:00
josh d3df67be1e Update rake 2021-05-18 16:03:14 -04:00
josh 791340b292 Build NFA for each token pattern 2021-05-17 22:57:18 -04:00
josh cf8718b69c Allow token definition with no pattern 2021-05-17 22:40:23 -04:00
josh 39f164a7db Parse . in a regex 2021-05-17 17:20:56 -04:00
josh 70b3e56de2 Store all characters as ranges; add CharacterClassUnit#to_nfa 2021-05-14 13:52:03 -04:00
josh 2e8e72a1e8 Add CharacterClassUnit and use it instead of AlternatesUnit 2021-05-14 12:32:53 -04:00
josh ea27baa630 Add #to_nfa for other regex unit types 2021-05-13 15:57:09 -04:00
josh d8dd64d860 Add NFA class
Start converting units to NFAs
2021-05-13 00:01:12 -04:00
josh 54cefda186 Use Parser 2021-05-11 16:52:28 -04:00
josh 201a38fb51 Add Parser specs 2021-05-11 15:29:40 -04:00
josh 33f9d01883 Rename start/end to min/max for CharacterRangeUnit 2021-05-11 15:28:45 -04:00
josh 9b09625c8a Fix parsing - at beginning of negated character class 2021-05-11 14:57:16 -04:00
josh 6119d860bc Fix character class parsing into an AlternatesUnit 2021-05-11 14:57:01 -04:00
josh 611ebeeddd Fix max multiplicity count parsing 2021-05-11 11:37:46 -04:00
josh 449eec4982 Fix multiplicity count parsing 2021-05-11 11:33:10 -04:00
josh 8cd648fc8f Create spec file for Parser 2021-05-07 16:58:38 -04:00
josh 885ef6c151 Rename Regex::Unit -> Regex::Parser 2021-05-07 16:57:05 -04:00
josh 60adffbbab Add rspec 2021-05-07 15:16:01 -04:00
josh b8c01ca1d1 Move Unit stuff from Imbecile::Regex to Imbecile::Regex::Unit 2021-05-07 15:10:51 -04:00
josh b04ff56308 Add Regex class 2021-05-02 15:22:45 -04:00
josh ca1d2d1e5c Fix class name determination from output file name 2021-05-01 17:01:15 -04:00
josh 13403405b0 Add Error class to handle grammar loading errors 2021-05-01 16:54:24 -04:00
josh 07dd68e367 Write output file from ERB template 2021-05-01 16:44:01 -04:00
josh c1666a1e74 Require output file on command line 2021-05-01 14:52:16 -04:00
josh 768a0ef17f Extract class name from grammar file 2021-05-01 14:34:00 -04:00
josh 9e865d1982 Throw error on unexpected grammar input line 2021-05-01 09:40:22 -04:00
josh 9884047090 Skip blank lines 2021-05-01 09:39:19 -04:00
josh 04393dcc51 Check for duplicate token names; skip comment lines 2021-05-01 09:38:08 -04:00
josh 7f27b3fd6f Exit with CLI exit code 2021-05-01 09:34:38 -04:00
josh 37ad87d602 Rename GrammarParser -> Grammar 2021-05-01 09:33:35 -04:00
josh 23b7782a5d Begin parsing grammar 2021-05-01 09:31:12 -04:00
josh 0cc4516c0e Add GrammarParser class to parse input file 2021-05-01 08:22:14 -04:00
josh 75a1049040 Parse command-line options 2021-05-01 08:16:09 -04:00
josh a9ff93dda4 Add script to test run 2021-05-01 08:16:01 -04:00
josh d879a93d09 Add bin/imbecile and Imbecile::CLI module 2021-04-29 23:26:52 -04:00
josh ee27c5e9b1 Add Gemfile.lock 2021-04-29 23:26:37 -04:00
josh 989e5f47de Edit some gemspec fields 2021-04-29 23:26:29 -04:00
josh 04e17cde30 Add "bundle gem"-generated files 2021-04-29 23:22:23 -04:00
josh bc217e7ddb Start on ruby branch 2021-04-29 23:18:22 -04:00
75 changed files with 5264 additions and 1317 deletions

No files matched your search

+11 -9
View File
@@ -1,9 +1,11 @@
imbecile
tags
*.o
.*.swp
*.dep
tmpl.*
tests/*/itest.cc
tests/*/itest.h
tests/*/test
/.bundle/
/.yardoc
/_yardoc/
/coverage/
/dist/
/gen/
/pkg/
/spec/reports/
/tmp/
/.rspec_status
/spec/run/
-3
View File
@@ -1,3 +0,0 @@
[submodule "refptr"]
path = refptr
url = http://github.com/holtrop/refptr.git
+3
View File
@@ -0,0 +1,3 @@
--format documentation
--color
--require spec_helper
+3
View File
@@ -0,0 +1,3 @@
## v1.0.0
- Initial release
+7
View File
@@ -0,0 +1,7 @@
source "https://rubygems.org"
gem "rake"
gem "rspec"
gem "rdoc"
gem "redcarpet"
gem "syntax"
+38
View File
@@ -0,0 +1,38 @@
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)
rspec-mocks (~> 3.11.0)
rspec-core (3.11.0)
rspec-support (~> 3.11.0)
rspec-expectations (3.11.0)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.11.0)
rspec-mocks (3.11.1)
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.3.7
+21
View File
@@ -0,0 +1,21 @@
The MIT License (MIT)
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
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
-61
View File
@@ -1,61 +0,0 @@
TARGET := imbecile
CXXOBJS := $(patsubst %.cc,%.o,$(wildcard *.cc)) tmpl.o
CXXDEPS := $(patsubst %.o,.%.dep,$(CXXOBJS))
CXXFLAGS := -O2
DEPS := $(CXXDEPS)
OBJS := $(CXXOBJS)
LDFLAGS := -lpcre
CPPFLAGS := -I$(shell pwd)/refptr
all: submodule_check tmpl.h $(TARGET)
.PHONY: submodule_check
submodule_check:
@if [ ! -e refptr/refptr.h ]; then \
echo Error: \"refptr\" folder is not populated.; \
echo Perhaps you forgot to do \"git checkout --recursive\"?; \
echo You can remedy the situation with \"git submodule update --init\".; \
exit 1; \
fi
$(TARGET): $(OBJS)
$(CXX) -o $@ $^ $(LDFLAGS)
# Object file rules
%.o: %.cc
$(CXX) -c -o $@ $(CPPFLAGS) $(CXXFLAGS) $<
# Make dependency files
.%.dep: %.c
@set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< | sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' > $@
.%.dep: %.cc tmpl.h
@set -e; rm -f $@; \
$(CXX) -MM $(CPPFLAGS) $< | sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' > $@
tmpl.cc: $(wildcard tmpl/*)
echo -n > $@
for f in $*/*; \
do xxd -i $$f >> $@; \
done
tmpl.h: tmpl.cc
echo '#ifndef $*_h' > $@
echo '#define $*_h' >> $@
grep '$*_' $^ | sed -e 's/^/extern /' -e 's/ =.*/;/' >> $@
echo '#endif' >> $@
.PHONY: tests
tests: PATH := $(shell pwd):$(PATH)
tests: all
$(MAKE) -C $@
tests-clean:
$(MAKE) -C tests clean
clean: tests-clean
-rm -f $(TARGET) *.o .*.dep tmpl.cc tmpl.h
-include $(CXXDEPS)
-423
View File
@@ -1,423 +0,0 @@
#include <stdio.h>
#include <string.h>
#include <pcre.h>
#include <ctype.h> /* toupper() */
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include "Parser.h"
#include "TokenDefinition.h"
#include "RuleDefinition.h"
#include "tmpl.h"
using namespace std;
#define DEBUG
Parser::Parser()
: m_classname("Parser"), m_namespace(""), m_extension("cc"),
m_token_data(new string()), m_token_code(new string()),
m_defines(new string())
{
}
void Parser::makeDefine(const string & defname, const string & definition)
{
*m_defines += string("#define ") + defname + " " + definition + "\n";
}
bool Parser::write(const string & fname)
{
if (m_tokens.size() < 1 || m_rules.size() < 1)
return false;
string header_fname = fname + ".h";
string body_fname = fname + "." + m_extension;
ofstream header(header_fname.c_str());
ofstream body(body_fname.c_str());
/* process data */
refptr<string> token_classes = new string();
refptr<string> token_classes_code = new string();
int i = 0;
for (list<TokenDefinitionRef>::const_iterator it = m_tokens.begin();
it != m_tokens.end();
it++)
{
char buff[20];
sprintf(buff, "%d", i++);
makeDefine((*it)->getIdentifier(), buff);
*token_classes += (*it)->getClassDefinition();
*token_classes_code += (*it)->getProcessMethod();
}
if (m_namespace != "")
{
makeDefine("I_NAMESPACE", m_namespace);
}
makeDefine("I_CLASSNAME", m_classname);
/* set up replacements */
setReplacement("token_list", buildTokenList());
setReplacement("buildToken", buildBuildToken());
setReplacement("header_name",
new string(string("\"") + header_fname + "\""));
setReplacement("token_code", m_token_code);
setReplacement("token_data", m_token_data);
setReplacement("defines", m_defines);
setReplacement("token_classes", token_classes);
setReplacement("token_classes_code", token_classes_code);
/* write the header */
writeTmpl(header, (char *) tmpl_parser_h, tmpl_parser_h_len);
/* write the body */
writeTmpl(body, (char *) tmpl_parser_cc, tmpl_parser_cc_len);
header.close();
body.close();
return true;
}
bool Parser::writeTmpl(std::ostream & out, char * dat, int len)
{
char * newline;
char * data = dat;
const char * errptr;
int erroffset;
data[len-1] = '\n';
const int ovec_size = 6;
int ovector[ovec_size];
pcre * replace = pcre_compile("{%(\\w+)%}", 0, &errptr, &erroffset, NULL);
while (data < (dat + len) && (newline = strstr(data, "\n")) != NULL)
{
if (pcre_exec(replace, NULL, data, newline - data,
0, 0, ovector, ovec_size) >= 0)
{
if (ovector[0] > 0)
{
out.write(data, ovector[0]);
}
out << *getReplacement(string(data, ovector[2],
ovector[3] - ovector[2]));
if (ovector[1] < newline - data)
{
out.write(data + ovector[1], newline - data - ovector[1]);
}
}
else
{
out.write(data, newline - data);
}
out << '\n';
data = newline + 1;
}
}
refptr<std::string> Parser::getReplacement(const std::string & name)
{
if (m_replacements.find(name) != m_replacements.end())
{
return m_replacements[name];
}
#ifdef DEBUG
cerr << "No replacement found for \"" << name << "\"" << endl;
#endif
return new string("");
}
refptr<string> Parser::buildTokenList()
{
refptr<string> tokenlist = new string();
for (list<TokenDefinitionRef>::const_iterator t = m_tokens.begin();
t != m_tokens.end();
t++)
{
if (t != m_tokens.begin())
*tokenlist += " ";
*tokenlist += "{ \"" + (*t)->getName() + "\", \""
+ (*t)->getCString() + "\", "
+ ((*t)->getProcessFlag() ? "true" : "false") + " }";
if (({typeof(t) tmp = t; ++tmp;}) != m_tokens.end())
*tokenlist += ",\n";
}
return tokenlist;
}
refptr<string> Parser::buildBuildToken()
{
refptr<string> buildToken = new string();
for (list<TokenDefinitionRef>::const_iterator t = m_tokens.begin();
t != m_tokens.end();
t++)
{
*buildToken += "case " + (*t)->getIdentifier() + ":\n";
*buildToken += " token = new " + (*t)->getClassName() + "();\n";
*buildToken += " break;\n";
}
return buildToken;
}
bool Parser::parseInputFile(char * buff, int size)
{
typedef pcre * pcre_ptr;
enum { none, tokens, rules };
pcre_ptr empty, comment, section_name, token, rule,
data_begin, data_end, code_begin, code_end;
struct { pcre_ptr * re; const char * pattern; } exprs[] = {
{&empty, "^\\s*$"},
{&comment, "^\\s*#"},
{&section_name, "^\\s*\\[([^\\]]+?)\\]\\s*$"},
{&token, "^\\s*" /* possible leading ws */
"([a-zA-Z_][a-zA-Z_0-9]*)" /* 1: token name */
"\\s+" /* required whitespace */
"((?:[^\\\\\\s]|\\\\.)+)"}, /* 2: token RE */
{&rule, "^\\s*(\\S+)\\s*:=(.*)$"},
{&data_begin, "^\\s*\\${"},
{&data_end, "\\$}"},
{&code_begin, "^\\s*%{"},
{&code_end, "%}"}
};
const int ovec_size = 3 * 10;
int ovector[ovec_size];
int lineno = 0;
char * newline;
char * input = buff;
string current_section_name;
map<string, int> sections;
sections["none"] = none;
sections["tokens"] = tokens;
sections["rules"] = rules;
int section = none;
string line;
bool append_line = false;
bool gathering_data = false;
bool gathering_code = false;
string gather;
bool continue_line = false;
TokenDefinitionRef current_token;
for (int i = 0; i < sizeof(exprs)/sizeof(exprs[0]); i++)
{
const char * errptr;
int erroffset;
*exprs[i].re = pcre_compile(exprs[i].pattern, 0,
&errptr, &erroffset, NULL);
if (*exprs[i].re == NULL)
{
cerr << "Error compiling regex '" << exprs[i].pattern <<
"': " << errptr << " at position " << erroffset << endl;
return false;
}
}
for (;;)
{
if (continue_line)
{
continue_line = false;
}
else
{
if ((newline = strstr(input, "\n")) == NULL)
break;
int line_length = newline - input;
if (line_length >= 1 && newline[-1] == '\r')
{
newline[-1] = '\n';
line_length--;
}
lineno++;
if (append_line)
{
line += string(input, line_length);
}
else
{
line = string(input, line_length);
}
input = newline + 1; /* set up for next loop iteration */
}
if ( (pcre_exec(empty, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
|| (pcre_exec(comment, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
)
{
/* skip empty or comment lines */;
continue;
}
if (! (gathering_code || gathering_data) )
{
if (line.size() > 0 && line[line.size()-1] == '\\')
{
line[line.size()-1] = ' ';
append_line = true;
continue;
}
else
{
append_line = false;
}
if (pcre_exec(section_name, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
{
current_section_name
= string(line, ovector[2], ovector[3] - ovector[2]);
if (sections.find(current_section_name) != sections.end())
{
section = sections[current_section_name];
}
else
{
cerr << "Unknown section name '" << current_section_name
<< "'!" << endl;
return false;
}
continue;
}
}
switch (section)
{
case none:
cerr << "Unrecognized input on line " << lineno << endl;
return false;
case tokens:
if (gathering_data)
{
if (pcre_exec(data_end, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
{
gather += string(line, 0, ovector[0]) + "\n";
gathering_data = false;
line = string(line, ovector[1]);
continue_line = true;
if (current_token.isNull())
{
*m_token_data += gather;
}
else
{
current_token->addData(gather);
}
}
else
{
gather += line + "\n";
}
continue;
}
else if (gathering_code)
{
if (pcre_exec(code_end, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
{
gather += string(line, 0, ovector[0]) + "\n";
gathering_code = false;
line = string(line, ovector[1]);
continue_line = true;
if (current_token.isNull())
{
*m_token_code += gather;
}
else
{
current_token->addCode(gather);
}
}
else
{
gather += line + "\n";
}
continue;
}
else if (pcre_exec(data_begin, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
{
gathering_data = true;
gather = "";
line = string(line, ovector[1]);
continue_line = true;
continue;
}
else if (pcre_exec(code_begin, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
{
gathering_code = true;
gather = "";
line = string(line, ovector[1]);
continue_line = true;
continue;
}
else if (pcre_exec(token, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
{
string name(line, ovector[2], ovector[3] - ovector[2]);
string definition(line,
ovector[4], ovector[5] - ovector[4]);
current_token = new TokenDefinition();
if (current_token->create(name, definition))
{
addTokenDefinition(current_token);
}
else
{
cerr << "Error in token definition ending on line "
<< lineno << endl;
return false;
}
line = string(line, ovector[1]);
continue_line = true;
continue;
}
else
{
cerr << "Unrecognized input on line " << lineno << endl;
return false;
}
break;
case rules:
if (pcre_exec(rule, NULL, line.c_str(), line.size(),
0, 0, ovector, ovec_size) >= 0)
{
string name(line, ovector[2], ovector[3] - ovector[2]);
string definition(line,
ovector[4], ovector[5] - ovector[4]);
refptr<RuleDefinition> rd = new RuleDefinition();
if (rd->create(name, definition))
{
addRuleDefinition(rd);
}
else
{
cerr << "Error in rule definition ending on line "
<< lineno << endl;
return false;
}
}
else
{
cerr << "Unrecognized input on line " << lineno << endl;
return false;
}
break;
}
}
for (int i = 0; i < sizeof(exprs)/sizeof(exprs[0]); i++)
{
pcre_free(*exprs[i].re);
}
return true;
}
-61
View File
@@ -1,61 +0,0 @@
#ifndef PARSER_H
#define PARSER_H
#include <vector>
#include <string>
#include <list>
#include <map>
#include "refptr.h"
#include "TokenDefinition.h"
#include "RuleDefinition.h"
class Parser
{
public:
Parser();
void addTokenDefinition(refptr<TokenDefinition> td)
{
m_tokens.push_back(td);
}
void addRuleDefinition(refptr<RuleDefinition> rd)
{
m_rules.push_back(rd);
}
bool write(const std::string & fname);
bool parseInputFile(char * buff, int size);
void setClassName(const std::string & cn) { m_classname = cn; }
std::string getClassName() { return m_classname; }
void setNamespace(const std::string & ns) { m_namespace = ns; }
std::string getNamespace() { return m_namespace; }
void setExtension(const std::string & e) { m_extension = e; }
std::string getExtension() { return m_extension; }
protected:
refptr<std::string> buildTokenList();
refptr<std::string> buildBuildToken();
bool writeTmpl(std::ostream & out, char * dat, int len);
refptr<std::string> getReplacement(const std::string & name);
void setReplacement(const std::string & name, refptr<std::string> val)
{
m_replacements[name] = val;
}
void makeDefine(const std::string & defname,
const std::string & definition);
std::list<TokenDefinitionRef> m_tokens;
std::vector< refptr< RuleDefinition > > m_rules;
std::string m_classname;
std::string m_namespace;
std::string m_extension;
std::map< std::string, refptr<std::string> > m_replacements;
refptr<std::string> m_token_data;
refptr<std::string> m_token_code;
refptr<std::string> m_defines;
};
#endif
-5
View File
@@ -1,5 +0,0 @@
Imbecile is a bottom-up parser generator. It targets C++ and automatically
generates a class heirarchy for interacting with the parser.
Imbecile generates both a lexer and a parser based on the rules given to
it in the input file.
+31
View File
@@ -0,0 +1,31 @@
# The Propane Parser Generator
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
## Installation
TODO
## Usage
TODO: Write usage instructions here
## Development
After checking out the repository, run `bundle install` to install dependencies.
Run `rake spec` to execute tests.
## Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/holtrop/propane.
## License
Propane is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
+18
View File
@@ -0,0 +1,18 @@
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]
end
end
task :default => :spec
desc "Build user guide"
task :user_guide do
system("ruby", "-Ilib", "rb/gen_user_guide.rb")
end
-9
View File
@@ -1,9 +0,0 @@
#include "RuleDefinition.h"
using namespace std;
bool RuleDefinition::create(const string & name, const string & definition)
{
m_name = name;
}
-16
View File
@@ -1,16 +0,0 @@
#ifndef RULEDEFINITION_H
#define RULEDEFINITION_H
#include <string>
class RuleDefinition
{
public:
bool create(const std::string & name, const std::string & definition);
protected:
std::string m_name;
};
#endif
-125
View File
@@ -1,125 +0,0 @@
#include <pcre.h>
#include <iostream>
#include <string>
#include <vector>
#include "TokenDefinition.h"
#include "refptr.h"
using namespace std;
#define WHITESPACE " \n\r\t\v"
static string trim(string s)
{
size_t lastpos = s.find_last_not_of(WHITESPACE);
if (lastpos == string::npos)
return "";
s.erase(lastpos + 1);
s.erase(0, s.find_first_not_of(WHITESPACE));
return s;
}
static refptr< vector<string> > split(const string & delim, string str)
{
refptr< vector<string> > ret = new vector<string>();
size_t pos;
while ( (pos = str.find(delim)) != string::npos )
{
string t = str.substr(0, pos);
ret->push_back(t);
str.erase(0, pos + 1);
}
if (str != "")
ret->push_back(str);
return ret;
}
static string c_escape(const string & orig)
{
string result;
for (string::const_iterator it = orig.begin(); it != orig.end(); it++)
{
if (*it == '\\' || *it == '"')
result += '\\';
result += *it;
}
return result;
}
TokenDefinition::TokenDefinition()
: m_process(false)
{
}
bool TokenDefinition::create(const string & name,
const string & definition)
{
const char * errptr;
int erroffset;
pcre * re = pcre_compile(definition.c_str(), 0, &errptr, &erroffset, NULL);
if (re == NULL)
{
cerr << "Error compiling regular expression '" << definition
<< "' at position " << erroffset << ": " << errptr << endl;
return false;
}
m_name = name;
m_definition = definition;
pcre_free(re);
#if 0
refptr< vector< string > > parts = split(",", flags);
for (int i = 0, sz = parts->size(); i < sz; i++)
{
(*parts)[i] = trim((*parts)[i]);
string & s = (*parts)[i];
if (s == "p")
{
m_process = true;
}
else
{
cerr << "Unknown token flag \"" << s << "\"" << endl;
return false;
}
}
#endif
return true;
}
string TokenDefinition::getCString() const
{
return c_escape(m_definition);
}
string TokenDefinition::getClassDefinition() const
{
string ret = "class "+ getClassName() + " : public Token {\n";
ret += "public:\n";
if (m_process)
{
ret += " virtual void process(const Matches & matches);\n";
}
ret += "\n";
ret += "protected:\n";
ret += m_data + "\n";
ret += "};\n";
return ret;
}
string TokenDefinition::getProcessMethod() const
{
string ret;
if (m_code != "")
{
ret += "void " + getClassName() + "::process(const Matches & matches) {\n";
ret += m_code + "\n";
ret += "}\n";
}
return ret;
}
-37
View File
@@ -1,37 +0,0 @@
#ifndef TOKENDEFINITION_H
#define TOKENDEFINITION_H
#include <string>
#include "refptr.h"
class TokenDefinition
{
public:
TokenDefinition();
bool create(const std::string & name,
const std::string & definition);
std::string getCString() const;
std::string getName() const { return m_name; }
bool getProcessFlag() const { return m_process; }
void setProcessFlag(bool p) { m_process = p; }
void addData(const std::string & d) { m_data += d; }
std::string getData() const { return m_data; }
void addCode(const std::string & c) { m_code += c; m_process = true; }
std::string getCode() const { return m_code; }
std::string getClassDefinition() const;
std::string getProcessMethod() const;
std::string getIdentifier() const { return "TK_" + m_name; }
std::string getClassName() const { return "Tk" + m_name; }
protected:
std::string m_name;
std::string m_definition;
bool m_process;
std::string m_data;
std::string m_code;
};
typedef refptr<TokenDefinition> TokenDefinitionRef;
#endif
+787
View File
@@ -0,0 +1,787 @@
<% if @grammar.modulename %>
module <%= @grammar.modulename %>;
<% end %>
import std.stdio;
import std.sumtype;
<% @grammar.code_blocks.each do |code| %>
<%= code %>
<% end %>
class <%= @classname %>
{
alias TokenID = uint;
enum : TokenID
{
<% @grammar.tokens.each_with_index do |token, 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_COUNT = <%= @grammar.tokens.size %>,
}
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
{
struct Success
{
CodePoint code_point;
uint code_point_length;
}
struct EOF {}
struct DecodeError {}
static SumType!(
Success,
EOF,
DecodeError)
decode_code_point(string input)
{
if (input.length == 0u)
{
return cast(ReturnType!decode_code_point)EOF();
}
char c = input[0];
CodePoint code_point;
uint code_point_length;
if ((c & 0x80u) == 0u)
{
code_point = c;
code_point_length = 1u;
}
else
{
ubyte following_bytes;
if ((c & 0xE0u) == 0xC0u)
{
code_point = c & 0x1Fu;
following_bytes = 1u;
}
else if ((c & 0xF0u) == 0xE0u)
{
code_point = c & 0x0Fu;
following_bytes = 2u;
}
else if ((c & 0xF8u) == 0xF0u)
{
code_point = c & 0x07u;
following_bytes = 3u;
}
else if ((c & 0xFCu) == 0xF8u)
{
code_point = c & 0x03u;
following_bytes = 4u;
}
else if ((c & 0xFEu) == 0xFCu)
{
code_point = c & 0x01u;
following_bytes = 5u;
}
else
{
return cast(ReturnType!decode_code_point)DecodeError();
}
if (input.length <= following_bytes)
{
return cast(ReturnType!decode_code_point)DecodeError();
}
code_point_length = following_bytes + 1u;
for (size_t i = 0u; i < following_bytes; i++)
{
char b = input[i + 1u];
if ((b & 0xC0u) != 0x80u)
{
return cast(ReturnType!decode_code_point)DecodeError();
}
code_point = (code_point << 6u) | (b & 0x3Fu);
}
}
return cast(ReturnType!decode_code_point)Success(code_point, code_point_length);
}
}
static class Lexer
{
private struct Transition
{
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;
Token token;
UserCodeID code_id;
bool drop;
bool accepts() const
{
return drop || token.is_valid() || code_id.is_valid();
}
}
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 immutable State[] states = [
<% state_table.each do |state_table_entry| %>
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 %>
];
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 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(string input)
{
m_input = input;
m_mode = <%= @lexer.mode_id("default") %>;
}
Result lex_token()
{
for (;;)
{
Result result = attempt_lex_token();
if (result.token < _TOKEN_COUNT)
{
return result;
}
}
}
/**
* 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)
{
switch (code_id)
{
<% @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;
}
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 (;;)
{
bool lex_continue = false;
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);
}
});
}
}
/**
* 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++)
{
if ((transitions[transition_table_index + i].first <= code_point) &&
(code_point <= transitions[transition_table_index + i].last))
{
uint destination = transitions[transition_table_index + i].destination;
return TransitionResult.found(destination);
}
}
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;
}
}
}
Executable
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env ruby
require "propane"
exit Propane::CLI.run(ARGV.dup)
+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}
-101
View File
@@ -1,101 +0,0 @@
#include <getopt.h>
#include <iostream>
#include <fstream>
#include "refptr.h"
#include "Parser.h"
using namespace std;
string buildOutputFilename(string & input_fname);
int main(int argc, char * argv[])
{
int longind = 1;
int opt;
Parser p;
string outfile;
static struct option longopts[] = {
/* name, has_arg, flag, val */
{ "classname", required_argument, NULL, 'c' },
{ "extension", required_argument, NULL, 'e' },
{ "namespace", required_argument, NULL, 'n' },
{ "outfile", required_argument, NULL, 'o' },
{ NULL, 0, NULL, 0 }
};
while ((opt = getopt_long(argc, argv, "", longopts, &longind)) != -1)
{
switch (opt)
{
case 'c': /* classname */
p.setClassName(optarg);
break;
case 'e': /* extension */
p.setExtension(optarg);
break;
case 'n': /* namespace */
p.setNamespace(optarg);
break;
case 'o': /* outfile */
outfile = optarg;
break;
}
}
if (optind >= argc)
{
cerr << "Usage: imbecile [options] <input-file>" << endl;
return 1;
}
string input_fname = argv[optind];
ifstream ifs;
ifs.open(input_fname.c_str(), ios::binary);
if (!ifs.is_open())
{
cerr << "Error opening input file: '" << input_fname << "'";
return 2;
}
ifs.seekg(0, ios_base::end);
int size = ifs.tellg();
ifs.seekg(0, ios_base::beg);
char * buff = new char[size];
ifs.read(buff, size);
ifs.close();
if (outfile == "")
outfile = buildOutputFilename(input_fname);
if (!p.parseInputFile(buff, size))
{
cerr << "Error parsing " << input_fname << endl;
return 3;
}
if (!p.write(outfile))
{
cerr << "Error processing " << input_fname << endl;
return 4;
}
delete[] buff;
return 0;
}
string buildOutputFilename(string & input_fname)
{
string outfile;
size_t len = input_fname.length();
if (len > 2 && input_fname.substr(len - 2) == ".I")
{
outfile = input_fname.substr(0, len - 2);
}
else
{
outfile = input_fname;
}
return outfile;
}
+47
View File
@@ -0,0 +1,47 @@
require "erb"
require "set"
require "stringio"
require_relative "propane/cli"
require_relative "propane/code_point_range"
require_relative "propane/fa"
require_relative "propane/fa/state"
require_relative "propane/fa/state/transition"
require_relative "propane/generator"
require_relative "propane/grammar"
require_relative "propane/lexer"
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
class Error < RuntimeError
end
class << self
def run(input_file, output_file, log_file)
begin
grammar = Grammar.new(File.read(input_file))
generator = Generator.new(grammar, output_file, log_file)
generator.generate
rescue Error => e
$stderr.puts e.message
return 2
end
return 0
end
end
end
+54
View File
@@ -0,0 +1,54 @@
class Propane
module CLI
USAGE = <<EOF
Usage: #{$0} [options] <input-file> <output-file>
Options:
--log LOG Write log file
--version Show program version and exit
-h, --help Show this usage and exit
EOF
class << self
def run(args)
params = []
log_file = nil
i = 0
while i < args.size
arg = args[i]
case arg
when "--log"
if i + 1 < args.size
i += 1
log_file = args[i]
end
when "--version"
puts "propane v#{VERSION}"
return 0
when "-h", "--help"
puts USAGE
return 0
when /^-/
$stderr.puts "Error: unknown option #{arg}"
return 1
else
params << arg
end
i += 1
end
if params.size != 2
$stderr.puts "Error: specify input and output files"
return 1
end
unless File.readable?(params[0])
$stderr.puts "Error: cannot read #{params[0]}"
return 2
end
Propane.run(*params, log_file)
end
end
end
end
+84
View File
@@ -0,0 +1,84 @@
class Propane
class CodePointRange
MAX_CODE_POINT = 0x7FFFFFFF
attr_reader :first
attr_reader :last
include Comparable
# Build a CodePointRange
def initialize(first, last = nil)
@first = first.ord
if last
@last = last.ord
if @last < @first
raise "Invalid CodePointRange: last code point must be > first code point"
end
else
@last = @first
end
end
def <=>(other)
if self.first != other.first
@first <=> other.first
else
@last <=> other.last
end
end
def include?(v)
if v.is_a?(CodePointRange)
@first <= v.first && v.last <= @last
else
@first <= v && v <= @last
end
end
def size
@last - @first + 1
end
class << self
def invert_ranges(code_point_ranges)
new_ranges = []
last_cp = -1
code_point_ranges.sort.each do |code_point_range|
if code_point_range.first > (last_cp + 1)
new_ranges << CodePointRange.new(last_cp + 1, code_point_range.first - 1)
last_cp = code_point_range.last
else
last_cp = [last_cp, code_point_range.last].max
end
end
if last_cp < MAX_CODE_POINT
new_ranges << CodePointRange.new(last_cp + 1, MAX_CODE_POINT)
end
new_ranges
end
def first_subrange(code_point_ranges)
code_point_ranges.sort.reduce do |result, code_point_range|
if code_point_range.include?(result.first)
if code_point_range.last < result.last
code_point_range
else
result
end
else
if code_point_range.first <= result.last
CodePointRange.new(result.first, code_point_range.first - 1)
else
result
end
end
end
end
end
end
end
+61
View File
@@ -0,0 +1,61 @@
class Propane
class FA
attr_reader :start_state
def initialize
@start_state = State.new
end
def to_s
chr = lambda do |value|
if value < 32 || value > 127
"{#{value}}"
else
value.chr
end
end
rv = ""
states = enumerate
states.each do |state, id|
accepts_s = state.accepts ? " #{state.accepts}" : ""
rv += "#{id}#{accepts_s}:\n"
state.transitions.each do |transition|
if transition.nil?
range_s = "nil"
else
range_s = chr[transition.code_point_range.first]
if transition.code_point_range.size > 1
range_s += "-" + chr[transition.code_point_range.last]
end
end
accepts_s = transition.destination.accepts ? " #{transition.destination.accepts}" : ""
rv += " #{range_s} => #{states[transition.destination]}#{accepts_s}\n"
end
end
rv
end
def enumerate
@_enumerated ||=
begin
id = 0
states = {}
visit = lambda do |state|
unless states.include?(state)
states[state] = id
id += 1
state.transitions.each do |transition|
visit[transition.destination]
end
end
end
visit[@start_state]
states
end
end
end
end
+51
View File
@@ -0,0 +1,51 @@
class Propane
class FA
class State
attr_accessor :accepts
attr_reader :transitions
def initialize
@transitions = []
end
def add_transition(code_point_range, destination)
@transitions << Transition.new(code_point_range, destination)
end
# Determine the set of states that can be reached by nil transitions.
# from this state.
#
# @return [Set<NFA::State>]
# Set of states.
def nil_transition_states
states = Set[self]
analyze_state = lambda do |state|
state.nil_transitions.each do |transition|
unless states.include?(transition.destination)
states << transition.destination
analyze_state[transition.destination]
end
end
end
analyze_state[self]
states
end
def nil_transitions
@transitions.select do |transition|
transition.nil?
end
end
def cp_transitions
@transitions.reject do |transition|
transition.nil?
end
end
end
end
end
+23
View File
@@ -0,0 +1,23 @@
class Propane
class FA
class State
class Transition
attr_reader :code_point_range
attr_reader :destination
def initialize(code_point_range, destination)
@code_point_range = code_point_range
@destination = destination
end
def nil?
@code_point_range.nil?
end
end
end
end
end
+225
View File
@@ -0,0 +1,225 @@
class Propane
class Generator
def initialize(grammar, output_file, log_file)
@grammar = grammar
@output_file = output_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
def generate
erb = ERB.new(File.read(File.join(File.dirname(File.expand_path(__FILE__)), "../../assets/parser.d.erb")), trim_mode: "<>")
result = erb.result(binding.clone)
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_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 = {}
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
# 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
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]
tokens_by_name[component]
elsif rule_sets[component]
rule_sets[component]
else
raise Error.new("Symbol #{component} not found")
end
end
end
determine_possibly_empty_rulesets!(rule_sets)
# 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.
#
# @param rule_sets [Hash]
# RuleSets.
#
# @return [void]
def determine_possibly_empty_rulesets!(rule_sets)
begin
newly_discovered_empty_rulesets = false
rule_sets.each do |name, rule_set|
unless rule_set.could_be_empty?
if could_rule_set_be_empty?(rule_set)
newly_discovered_empty_rulesets = true
rule_set.could_be_empty = true
end
end
end
end while newly_discovered_empty_rulesets
end
# Determine whether a RuleSet could be empty.
#
# @param rule_set [RuleSet]
# RuleSet to test.
#
# @return [Boolean]
# Whether the RuleSet could be empty.
def could_rule_set_be_empty?(rule_set)
rule_set.rules.any? do |rule|
could_rule_be_empty?(rule)
end
end
# Determine whether a Rule could be empty.
#
# @param rule [Rule]
# Rule to test.
#
# @return [Boolean]
# Whether the Rule could be empty.
def could_rule_be_empty?(rule)
i = 0
loop do
if i == rule.components.size
return true
end
if rule.components[i].is_a?(Token)
return false
end
if !rule.components[i].could_be_empty?
return false
end
i += 1
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
+249
View File
@@ -0,0 +1,249 @@
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 :modulename
attr_reader :patterns
attr_reader :rules
attr_reader :tokens
attr_reader :code_blocks
attr_reader :ptypes
def initialize(input)
@patterns = []
@tokens = []
@rules = []
@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!
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
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
end
end
+69
View File
@@ -0,0 +1,69 @@
class Propane
class Lexer
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 = []
mode_table = []
@modes.each do |mode_name, mode_info|
state_table_offset = state_table.size
mode_table << {
state_table_offset: state_table_offset,
}
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, 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
end
+89
View File
@@ -0,0 +1,89 @@
class Propane
class Lexer
class DFA < FA
def initialize(patterns)
super()
start_nfa = Regex::NFA.new
patterns.each do |pattern|
start_nfa.start_state.add_transition(nil, pattern.nfa.start_state)
end
@nfa_state_sets = {}
@states = []
@to_process = Set.new
nil_transition_states = start_nfa.start_state.nil_transition_states
register_nfa_state_set(nil_transition_states)
while @to_process.size > 0
state_set = @to_process.first
@to_process.delete(state_set)
process_nfa_state_set(state_set)
end
@start_state = @states[0]
end
private
def register_nfa_state_set(nfa_state_set)
unless @nfa_state_sets.include?(nfa_state_set)
state_id = @states.size
@nfa_state_sets[nfa_state_set] = state_id
@states << State.new
@to_process << nfa_state_set
end
end
def process_nfa_state_set(nfa_state_set)
state_id = @nfa_state_sets[nfa_state_set]
state = @states[state_id]
if state_id > 0
nfa_state_set.each do |nfa_state|
if nfa_state.accepts
if state.accepts
if nfa_state.accepts.line_number < state.accepts.line_number
state.accepts = nfa_state.accepts
end
else
state.accepts = nfa_state.accepts
end
end
end
end
transitions = transitions_for(nfa_state_set)
while transitions.size > 0
subrange = CodePointRange.first_subrange(transitions.map(&:code_point_range))
dest_nfa_states = transitions.reduce(Set.new) do |result, transition|
if transition.code_point_range.include?(subrange)
result << transition.destination
end
result
end
dest_nfa_states = dest_nfa_states.reduce(Set.new) do |result, dest_nfa_state|
result + dest_nfa_state.nil_transition_states
end
register_nfa_state_set(dest_nfa_states)
dest_state = @states[@nfa_state_sets[dest_nfa_states]]
state.add_transition(subrange, dest_state)
transitions.delete_if do |transition|
transition.code_point_range.last <= subrange.last
end
transitions.map! do |transition|
if transition.code_point_range.first <= subrange.last
Regex::NFA::State::Transition.new(CodePointRange.new(subrange.last + 1, transition.code_point_range.last), transition.destination)
else
transition
end
end
end
end
def transitions_for(nfa_state_set)
nfa_state_set.reduce([]) do |result, state|
result + state.cp_transitions
end
end
end
end
end
+256
View File
@@ -0,0 +1,256 @@
class Propane
class Parser
def initialize(grammar, rule_sets, log)
@grammar = grammar
@rule_sets = rule_sets
@log = log
@item_sets = []
@item_sets_set = {}
start_item = Item.new(grammar.rules.first, 0)
eval_item_sets = Set[ItemSet.new([start_item])]
while eval_item_sets.size > 0
item_set = eval_item_sets.first
eval_item_sets.delete(item_set)
unless @item_sets_set.include?(item_set)
item_set.id = @item_sets.size
@item_sets << item_set
@item_sets_set[item_set] = item_set
item_set.following_symbols.each do |following_symbol|
unless following_symbol.name == "$EOF"
following_set = item_set.build_following_item_set(following_symbol)
eval_item_sets << following_set
end
end
end
end
@item_sets.each do |item_set|
process_item_set(item_set)
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.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: 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, reduce_table]
end
private
def process_item_set(item_set)
item_set.following_symbols.each do |following_symbol|
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
end
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
+146
View File
@@ -0,0 +1,146 @@
class Propane
class Parser
# Represent a parser "item", which is a position in a Rule that the parser
# could potentially be at.
class Item
# @return [Rule]
# The Rule corresponding to this Item.
attr_reader :rule
# @return [Integer]
# The parse position in this item.
attr_reader :position
# Construct an Item.
#
# @param rule [Rule]
# The Rule corresponding to this Item.
# @param position [Integer]
# The parse position in this Item.
def initialize(rule, position)
@rule = rule
@position = position
end
# Hash function.
#
# @return [Integer]
# Hash code.
def hash
[@rule, @position].hash
end
# Compare Item objects.
#
# @param other [Item]
# Item to compare to.
#
# @return [Boolean]
# Whether the Items are equal.
def ==(other)
@rule == other.rule && @position == other.position
end
# Compare Item objects.
#
# @param other [Item]
# Item to compare to.
#
# @return [Boolean]
# Whether the Items are equal.
def eql?(other)
self == other
end
# Return the set of Items obtained by "closing" the current item.
#
# If the following symbol for the current item is another Rule name, then
# this method will return all Items for that Rule with a position of 0.
# Otherwise, an empty Array is returned.
#
# @return [Array<Item>]
# Items obtained by "closing" the current item.
def closed_items
if @rule.components[@position].is_a?(RuleSet)
@rule.components[@position].rules.map do |rule|
Item.new(rule, 0)
end
else
[]
end
end
# Return whether the item is "complete", meaning that the parse position
# marker is at the end of the rule.
#
# @return [Boolean]
# Whether the item is "complete".
def complete?
@position == @rule.components.size
end
# Get the following symbol for the Item.
#
# 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(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.
#
# @param symbol [Token, RuleSet]
# Symbol to query.
#
# @return [Boolean]
# Whether this Item is followed by the provided symbol.
def followed_by?(symbol)
following_symbol == symbol
end
# Get the following item for this Item.
#
# That is, the Item formed by moving the parse position marker one place
# forward from its position in this Item.
#
# @return [Item]
# The following item for this Item.
def following_item
Item.new(@rule, @position + 1)
end
# Represent the Item as a String.
#
# @return [String]
# The Item represented as a String.
def to_s
parts = @rule.components.map(&:name)
parts[@position, 0] = "."
"#{@rule.name} -> #{parts.join(" ")}"
end
end
end
end
+146
View File
@@ -0,0 +1,146 @@
class Propane
class Parser
# Represent a parser "item set", which is a set of possible items that the
# parser could currently be parsing.
class ItemSet
# @return [Set<Item>]
# Items in this ItemSet.
attr_reader :items
# @return [Integer]
# ID of this ItemSet.
attr_accessor :id
# @return [Hash]
# Maps a following symbol to its ItemSet.
attr_reader :following_item_set
# @return [Set<ItemSet>]
# 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>]
# Items in this ItemSet.
def initialize(items)
@items = Set.new(items)
@following_item_set = {}
@in_sets = Set.new
close!
end
# Get the set of following symbols for all Items in this ItemSet.
#
# @return [Set<Token, RuleSet>]
# Set of following symbols for all Items in this ItemSet.
def following_symbols
Set.new(@items.map(&:following_symbol).compact)
end
# Build a following ItemSet for the given following symbol.
#
# @param symbol [Token, RuleSet]
# Following symbol to build the following ItemSet for.
#
# @return [ItemSet]
# Following ItemSet for the given following symbol.
def build_following_item_set(symbol)
ItemSet.new(items_followed_by(symbol).map(&:following_item))
end
# Hash function.
#
# @return [Integer]
# Hash code.
def hash
@items.hash
end
# Compare ItemSet objects.
#
# @param other [ItemSet]
# ItemSet to compare to.
#
# @return [Boolean]
# Whether the ItemSets are equal.
def ==(other)
@items.eql?(other.items)
end
# Compare ItemSet objects.
#
# @param other [ItemSet]
# ItemSet to compare to.
#
# @return [Boolean]
# Whether the ItemSets are equal.
def eql?(other)
self == other
end
# Set of ItemSets that lead to this ItemSet.
#
# This set includes this ItemSet.
#
# @return [Set<ItemSet>]
# Set of all ItemSets that lead up to this ItemSet.
def leading_item_sets
@in_sets.reduce(Set[self]) do |result, item_set|
result + item_set.leading_item_sets
end
end
# Represent the ItemSet as a String.
#
# @return [String]
# The ItemSet represented as a String.
def to_s
@items.map(&:to_s).join("\n")
end
private
# Close the ItemSet.
#
# This is done by recursively adding the closed Items for each Item in
# the ItemSet.
#
# @return [void]
def close!
eval_items = @items.dup
while eval_items.size > 0
item = eval_items.first
eval_items.delete(item)
item.closed_items.each do |new_item|
unless @items.include?(new_item)
eval_items << new_item
end
end
@items += eval_items
end
end
# Get the Items followed by the given following symbol.
#
# @param symbol [Token, RuleSet]
# Following symbol.
#
# @return [Array<Item>]
# Items followed by the given following symbol.
def items_followed_by(symbol)
@items.select do |item|
item.followed_by?(symbol)
end
end
end
end
end
+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
+162
View File
@@ -0,0 +1,162 @@
class Propane
class Regex
attr_reader :unit
attr_reader :nfa
def initialize(pattern)
@pattern = pattern.dup
@unit = parse_alternates
@nfa = @unit.to_nfa
if @pattern != ""
raise Error.new(%[Unexpected "#{@pattern}" in pattern])
end
end
private
def parse_alternates
au = AlternatesUnit.new
while @pattern != ""
c = @pattern[0]
return au if c == ")"
@pattern.slice!(0)
case c
when "["
au << parse_character_class
when "("
au << parse_group
when "*", "+", "?", "{"
if last_unit = au.last_unit
case c
when "*"
min_count, max_count = 0, nil
when "+"
min_count, max_count = 1, nil
when "?"
min_count, max_count = 0, 1
when "{"
min_count, max_count = parse_curly_count
end
mu = MultiplicityUnit.new(last_unit, min_count, max_count)
au.replace_last!(mu)
else
raise Error.new("#{c} follows nothing")
end
when "|"
au.new_alternate!
when "\\"
au << parse_backslash
when "."
au << period_character_class
else
au << CharacterRangeUnit.new(c)
end
end
au
end
def parse_group
au = parse_alternates
if @pattern[0] != ")"
raise Error.new("Unterminated group in pattern")
end
@pattern.slice!(0)
au
end
def parse_character_class
ccu = CharacterClassUnit.new
index = 0
loop do
if @pattern == ""
raise Error.new("Unterminated character class")
end
c = @pattern.slice!(0)
if c == "]"
break
elsif c == "^" && index == 0
ccu.negate = true
elsif c == "-" && (ccu.size == 0 || @pattern[0] == "]")
ccu << CharacterRangeUnit.new(c)
elsif c == "\\"
ccu << parse_backslash
elsif c == "-" && @pattern[0] != "]"
begin_cu = ccu.last_unit
unless begin_cu.is_a?(CharacterRangeUnit) && begin_cu.code_point_range.size == 1
raise Error.new("Character range must be between single characters")
end
if @pattern[0] == "\\"
@pattern.slice!(0)
end_cu = parse_backslash
unless end_cu.is_a?(CharacterRangeUnit) && end_cu.code_point_range.size == 1
raise Error.new("Character range must be between single characters")
end
max_code_point = end_cu.code_point
else
max_code_point = @pattern[0].ord
@pattern.slice!(0)
end
cru = CharacterRangeUnit.new(begin_cu.first, max_code_point)
ccu.replace_last!(cru)
else
ccu << CharacterRangeUnit.new(c)
end
index += 1
end
ccu
end
def parse_curly_count
if @pattern =~ /^(\d+)(?:(,)(\d*))?\}(.*)$/
min_count, comma, max_count, pattern = $1, $2, $3, $4
min_count = min_count.to_i
if comma.to_s == ""
max_count = min_count
elsif max_count.to_s != ""
max_count = max_count.to_i
if max_count < min_count
raise Error.new("Maximum repetition count cannot be less than minimum repetition count")
end
else
max_count = nil
end
@pattern = pattern
[min_count, max_count]
else
raise Error.new("Unexpected match count at #{@pattern}")
end
end
def parse_backslash
if @pattern == ""
raise Error.new("Error: unfollowed \\")
else
c = @pattern.slice!(0)
case c
when "d"
CharacterRangeUnit.new("0", "9")
when "s"
ccu = CharacterClassUnit.new
ccu << CharacterRangeUnit.new(" ")
ccu << CharacterRangeUnit.new("\t")
ccu << CharacterRangeUnit.new("\r")
ccu << CharacterRangeUnit.new("\n")
ccu << CharacterRangeUnit.new("\f")
ccu << CharacterRangeUnit.new("\v")
ccu
else
CharacterRangeUnit.new(c)
end
end
end
def period_character_class
ccu = CharacterClassUnit.new
ccu << CharacterRangeUnit.new(0, "\n".ord - 1)
ccu << CharacterRangeUnit.new("\n".ord + 1, 0xFFFFFFFF)
ccu
end
end
end
+26
View File
@@ -0,0 +1,26 @@
class Propane
class Regex
class NFA < FA
attr_reader :end_state
def initialize
super()
@end_state = State.new
end
class << self
def empty
nfa = NFA.new
nfa.start_state.add_transition(nil, nfa.end_state)
nfa
end
end
end
end
end
+172
View File
@@ -0,0 +1,172 @@
class Propane
class Regex
class Unit
end
class SequenceUnit < Unit
attr_accessor :units
def initialize
@units = []
end
def method_missing(*args)
@units.__send__(*args)
end
def to_nfa
if @units.empty?
NFA.empty
else
nfa = NFA.new
unit_nfas = @units.map do |unit|
unit.to_nfa
end
nfa.start_state.add_transition(nil, unit_nfas[0].start_state)
unit_nfas.reduce do |prev_nfa, next_nfa|
prev_nfa.end_state.add_transition(nil, next_nfa.start_state)
next_nfa
end.end_state.add_transition(nil, nfa.end_state)
nfa
end
end
end
class AlternatesUnit < Unit
attr_accessor :alternates
def initialize
@alternates = []
new_alternate!
end
def new_alternate!
@alternates << SequenceUnit.new
end
def <<(unit)
@alternates[-1] << unit
end
def last_unit
@alternates[-1][-1]
end
def replace_last!(new_unit)
@alternates[-1][-1] = new_unit
end
def to_nfa
if @alternates.size == 0
NFA.empty
elsif @alternates.size == 1
@alternates[0].to_nfa
else
nfa = NFA.new
alternate_nfas = @alternates.map do |alternate|
alternate.to_nfa
end
alternate_nfas.each do |alternate_nfa|
nfa.start_state.add_transition(nil, alternate_nfa.start_state)
alternate_nfa.end_state.add_transition(nil, nfa.end_state)
end
nfa
end
end
end
class CharacterRangeUnit < Unit
attr_reader :code_point_range
def initialize(c1, c2 = nil)
@code_point_range = CodePointRange.new(c1, c2)
end
def first
@code_point_range.first
end
def last
@code_point_range.last
end
def to_nfa
nfa = NFA.new
nfa.start_state.add_transition(@code_point_range, nfa.end_state)
nfa
end
end
class CharacterClassUnit < Unit
attr_accessor :units
attr_accessor :negate
def initialize
@units = []
@negate = false
end
def initialize
@units = []
end
def method_missing(*args)
@units.__send__(*args)
end
def <<(thing)
if thing.is_a?(CharacterClassUnit)
thing.each do |ccu_unit|
@units << ccu_unit
end
else
@units << thing
end
end
def last_unit
@units[-1]
end
def replace_last!(new_unit)
@units[-1] = new_unit
end
def to_nfa
nfa = NFA.new
if @units.empty?
nfa.start_state.add_transition(nil, nfa.end_state)
else
code_point_ranges = @units.map(&:code_point_range)
if @negate
code_point_ranges = CodePointRange.invert_ranges(code_point_ranges)
end
code_point_ranges.each do |code_point_range|
nfa.start_state.add_transition(code_point_range, nfa.end_state)
end
end
nfa
end
end
class MultiplicityUnit < Unit
attr_accessor :unit
attr_accessor :min_count
attr_accessor :max_count
def initialize(unit, min_count, max_count)
@unit = unit
@min_count = min_count
@max_count = max_count
end
def to_nfa
nfa = NFA.new
last_state = nfa.start_state
unit_nfa = nil
@min_count.times do
unit_nfa = @unit.to_nfa
last_state.add_transition(nil, unit_nfa.start_state)
last_state = unit_nfa.end_state
end
last_state.add_transition(nil, nfa.end_state)
if @max_count.nil?
if @min_count == 0
unit_nfa = @unit.to_nfa
last_state.add_transition(nil, unit_nfa.start_state)
end
unit_nfa.end_state.add_transition(nil, unit_nfa.start_state)
unit_nfa.end_state.add_transition(nil, nfa.end_state)
else
(@max_count - @min_count).times do
unit_nfa = @unit.to_nfa
last_state.add_transition(nil, unit_nfa.start_state)
unit_nfa.end_state.add_transition(nil, nfa.end_state)
last_state = unit_nfa.end_state
end
end
nfa
end
end
end
end
+73
View File
@@ -0,0 +1,73 @@
class Propane
class Rule
# @return [Array<Token, RuleSet>]
# Rule components.
attr_reader :components
# @return [String]
# User code associated with the rule.
attr_reader :code
# @return [Integer]
# Rule 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.
attr_reader :line_number
# @return [String]
# 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]
# Rule name.
# @param components [Array<String>]
# 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.
def initialize(name, components, code, ptypename, line_number)
@name = name
@components = components
@code = code
@ptypename = ptypename
@line_number = line_number
end
# Return whether the Rule is empty.
#
# A Rule is empty if it has no components.
#
# @return [Boolean]
# Whether the Rule is empty.
def empty?
@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
+80
View File
@@ -0,0 +1,80 @@
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
# @return [Boolean]
# Whether the RuleSet could expand to an empty sequence.
attr_writer :could_be_empty
# Construct a RuleSet.
#
# @param name [String]
# Name of the RuleSet.
# @param id [Integer]
# ID of the RuleSet.
def initialize(name, id)
@id = id
@name = name
@rules = []
@could_be_empty = false
end
# Add a Rule to the RuleSet.
#
# @param rule [Rule]
# Rule to add.
def <<(rule)
@rules << rule
end
# Return whether any Rule in the RuleSet is empty.
#
# @return [Boolean]
# Whether any rule in the RuleSet is empty.
def could_be_empty?
@could_be_empty
end
# Build the start token set for the RuleSet.
#
# @return [Set<Token>]
# Start token set for the RuleSet.
def start_token_set
if @_start_token_set.nil?
@_start_token_set = Set.new
@rules.each do |rule|
rule.components.each do |component|
if component.is_a?(Token)
@_start_token_set << component
break
else
@_start_token_set += component.start_token_set
unless component.could_be_empty?
break
end
end
end
end
end
@_start_token_set
end
end
end
+61
View File
@@ -0,0 +1,61 @@
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]
# Parser value type name.
attr_accessor :ptypename
# @return [Integer, nil]
# Token ID.
attr_accessor :id
# @return [Integer, nil]
# Line number where the token was defined in the input grammar.
attr_reader :line_number
# Construct a Token.
#
# @param name [String, nil]
# Token name.
# @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(name, ptypename, line_number)
@name = name
@ptypename = ptypename
@line_number = line_number
end
# 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
self.class.code_name(@name)
end
def to_s
@name
end
end
end
+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
+3
View File
@@ -0,0 +1,3 @@
class Propane
VERSION = "0.1.0"
end
Executable
+2
View File
@@ -0,0 +1,2 @@
#!/bin/sh
exec bundle exec ruby -Ilib bin/propane "$@"
+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)
Submodule refptr deleted from e2c7e88824.
+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
{
}
+87
View File
@@ -0,0 +1,87 @@
class Propane
describe CodePointRange do
describe "#<=>" do
it "sorts ranges" do
arr = [
CodePointRange.new(100,102),
CodePointRange.new(65, 68),
CodePointRange.new(65, 65),
CodePointRange.new(100, 100),
CodePointRange.new(68, 70),
]
arr.sort!
expect(arr[0]).to eq CodePointRange.new(65, 65)
expect(arr[1]).to eq CodePointRange.new(65, 68)
expect(arr[2]).to eq CodePointRange.new(68, 70)
expect(arr[3]).to eq CodePointRange.new(100, 100)
expect(arr[4]).to eq CodePointRange.new(100, 102)
end
end
describe "#include?" do
it "returns whether the code point is included in the range" do
expect(CodePointRange.new(100).include?(100)).to be_truthy
expect(CodePointRange.new(100, 100).include?(99)).to be_falsey
expect(CodePointRange.new(100, 100).include?(101)).to be_falsey
expect(CodePointRange.new(100, 120).include?(99)).to be_falsey
expect(CodePointRange.new(100, 120).include?(100)).to be_truthy
expect(CodePointRange.new(100, 120).include?(110)).to be_truthy
expect(CodePointRange.new(100, 120).include?(120)).to be_truthy
expect(CodePointRange.new(100, 120).include?(121)).to be_falsey
end
it "returns whether the range is included in the range" do
expect(CodePointRange.new(100).include?(CodePointRange.new(100))).to be_truthy
expect(CodePointRange.new(100, 100).include?(CodePointRange.new(99))).to be_falsey
expect(CodePointRange.new(100, 100).include?(CodePointRange.new(99, 100))).to be_falsey
expect(CodePointRange.new(100, 120).include?(CodePointRange.new(90, 110))).to be_falsey
expect(CodePointRange.new(100, 120).include?(CodePointRange.new(110, 130))).to be_falsey
expect(CodePointRange.new(100, 120).include?(CodePointRange.new(100, 120))).to be_truthy
expect(CodePointRange.new(100, 120).include?(CodePointRange.new(100, 110))).to be_truthy
expect(CodePointRange.new(100, 120).include?(CodePointRange.new(110, 120))).to be_truthy
expect(CodePointRange.new(100, 120).include?(CodePointRange.new(102, 118))).to be_truthy
end
end
describe ".invert_ranges" do
it "inverts ranges" do
expect(CodePointRange.invert_ranges(
[CodePointRange.new(60, 90),
CodePointRange.new(80, 85),
CodePointRange.new(80, 100),
CodePointRange.new(101),
CodePointRange.new(200, 300)])).to eq [
CodePointRange.new(0, 59),
CodePointRange.new(102, 199),
CodePointRange.new(301, 0x7FFFFFFF)]
expect(CodePointRange.invert_ranges(
[CodePointRange.new(0, 500),
CodePointRange.new(7000, 0x7FFFFFFF)])).to eq [
CodePointRange.new(501, 6999)]
end
end
describe ".first_subrange" do
it "returns the first subrange to split" do
expect(CodePointRange.first_subrange(
[CodePointRange.new(65, 90),
CodePointRange.new(66, 66),
CodePointRange.new(80, 90)])).to eq CodePointRange.new(65)
expect(CodePointRange.first_subrange(
[CodePointRange.new(65, 90)])).to eq CodePointRange.new(65, 90)
expect(CodePointRange.first_subrange(
[CodePointRange.new(65, 90),
CodePointRange.new(80, 90)])).to eq CodePointRange.new(65, 79)
expect(CodePointRange.first_subrange(
[CodePointRange.new(65, 90),
CodePointRange.new(65, 100),
CodePointRange.new(65, 95)])).to eq CodePointRange.new(65, 90)
expect(CodePointRange.first_subrange(
[CodePointRange.new(100, 120),
CodePointRange.new(70, 90)])).to eq CodePointRange.new(70, 90)
end
end
end
end
+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
+131
View File
@@ -0,0 +1,131 @@
class TestLexer
def initialize(token_dfa)
@token_dfa = token_dfa
end
def lex(input)
input_chars = input.chars
output = []
while lexed_token = lex_token(input_chars)
output << lexed_token
input_chars.slice!(0, lexed_token[1].size)
end
unless input_chars.empty?
raise "Unmatched input #{input_chars.join(" ")}"
end
output
end
def lex_token(input_chars)
return nil if input_chars.empty?
s = ""
current_state = @token_dfa.start_state
last_accepts = nil
last_s = nil
input_chars.each_with_index do |input_char, index|
if next_state = transition(current_state, input_char)
s += input_char
current_state = next_state
if current_state.accepts
last_accepts = current_state.accepts
last_s = s
end
else
break
end
end
if last_accepts
name = last_accepts.token ? last_accepts.token.name : nil
[name, last_s]
end
end
def transition(state, input_char)
state.transitions.each do |transition|
if transition.code_point_range.include?(input_char.ord)
return transition.destination
end
end
nil
end
end
def run(grammar, input)
grammar = Propane::Grammar.new(grammar)
token_dfa = Propane::Lexer::DFA.new(grammar.patterns)
test_lexer = TestLexer.new(token_dfa)
test_lexer.lex(input)
end
describe Propane::Lexer::DFA do
it "lexes a simple token" do
expect(run(<<EOF, "foo")).to eq [["foo", "foo"]]
token foo;
EOF
end
it "lexes two tokens" do
expected = [
["foo", "foo"],
["bar", "bar"],
]
expect(run(<<EOF, "foobar")).to eq expected
token foo;
token bar;
EOF
end
it "lexes the longer of multiple options" do
expected = [
["identifier", "foobar"],
]
expect(run(<<EOF, "foobar")).to eq expected
token foo;
token bar;
token identifier /[a-z]+/;
EOF
expected = [
["plusplus", "++"],
["plus", "+"],
]
expect(run(<<EOF, "+++")).to eq expected
token plus /\\+/;
token plusplus /\\+\\+/;
EOF
end
it "lexes whitespace" do
expected = [
["foo", "foo"],
["WS", " \t"],
["bar", "bar"],
]
expect(run(<<EOF, "foo \tbar")).to eq expected
token foo;
token bar;
token WS /\\s+/;
EOF
end
it "allows dropping a matched pattern" do
expected = [
["foo", "foo"],
[nil, " \t"],
["bar", "bar"],
]
expect(run(<<EOF, "foo \tbar")).to eq expected
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
+19
View File
@@ -0,0 +1,19 @@
class Propane
class Parser
describe Item do
it "operates properly with a set" do
rule = Object.new
item1 = Item.new(rule, 2)
item2 = Item.new(rule, 2)
expect(item1).to eq item2
expect(item1.eql?(item2)).to be_truthy
set = Set.new([item1, item2])
expect(set.size).to eq 1
end
end
end
end
+333
View File
@@ -0,0 +1,333 @@
class Propane
RSpec.describe Regex do
it "parses an empty expression" do
regex = Regex.new("")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0].size).to eq 0
end
it "parses a single character unit expression" do
regex = Regex.new("a")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterRangeUnit
end
it "parses a group with a single character unit expression" do
regex = Regex.new("(a)")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::AlternatesUnit
alt_unit = seq_unit[0]
expect(alt_unit.alternates.size).to eq 1
expect(alt_unit.alternates[0]).to be_a Regex::SequenceUnit
expect(alt_unit.alternates[0][0]).to be_a Regex::CharacterRangeUnit
end
it "parses a *" do
regex = Regex.new("a*")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::MultiplicityUnit
m_unit = seq_unit[0]
expect(m_unit.min_count).to eq 0
expect(m_unit.max_count).to be_nil
expect(m_unit.unit).to be_a Regex::CharacterRangeUnit
end
it "parses a +" do
regex = Regex.new("a+")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::MultiplicityUnit
m_unit = seq_unit[0]
expect(m_unit.min_count).to eq 1
expect(m_unit.max_count).to be_nil
expect(m_unit.unit).to be_a Regex::CharacterRangeUnit
end
it "parses a ?" do
regex = Regex.new("a?")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::MultiplicityUnit
m_unit = seq_unit[0]
expect(m_unit.min_count).to eq 0
expect(m_unit.max_count).to eq 1
expect(m_unit.unit).to be_a Regex::CharacterRangeUnit
end
it "parses a multiplicity count" do
regex = Regex.new("a{5}")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::MultiplicityUnit
m_unit = seq_unit[0]
expect(m_unit.min_count).to eq 5
expect(m_unit.max_count).to eq 5
expect(m_unit.unit).to be_a Regex::CharacterRangeUnit
end
it "parses a minimum-only multiplicity count" do
regex = Regex.new("a{5,}")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::MultiplicityUnit
m_unit = seq_unit[0]
expect(m_unit.min_count).to eq 5
expect(m_unit.max_count).to be_nil
expect(m_unit.unit).to be_a Regex::CharacterRangeUnit
end
it "parses a minimum and maximum multiplicity count" do
regex = Regex.new("a{5,8}")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::MultiplicityUnit
m_unit = seq_unit[0]
expect(m_unit.min_count).to eq 5
expect(m_unit.max_count).to eq 8
expect(m_unit.unit).to be_a Regex::CharacterRangeUnit
expect(m_unit.unit.first).to eq "a".ord
end
it "parses an escaped *" do
regex = Regex.new("a\\*")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 2
expect(seq_unit[0]).to be_a Regex::CharacterRangeUnit
expect(seq_unit[0].first).to eq "a".ord
expect(seq_unit[1]).to be_a Regex::CharacterRangeUnit
expect(seq_unit[1].first).to eq "*".ord
end
it "parses an escaped +" do
regex = Regex.new("a\\+")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 2
expect(seq_unit[0]).to be_a Regex::CharacterRangeUnit
expect(seq_unit[0].first).to eq "a".ord
expect(seq_unit[1]).to be_a Regex::CharacterRangeUnit
expect(seq_unit[1].first).to eq "+".ord
end
it "parses an escaped \\" do
regex = Regex.new("\\\\d")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 2
expect(seq_unit[0]).to be_a Regex::CharacterRangeUnit
expect(seq_unit[0].first).to eq "\\".ord
expect(seq_unit[1]).to be_a Regex::CharacterRangeUnit
expect(seq_unit[1].first).to eq "d".ord
end
it "parses a character class" do
regex = Regex.new("[a-z_]")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterClassUnit
ccu = seq_unit[0]
expect(ccu.negate).to be_falsey
expect(ccu.size).to eq 2
expect(ccu[0]).to be_a Regex::CharacterRangeUnit
expect(ccu[0].first).to eq "a".ord
expect(ccu[0].last).to eq "z".ord
expect(ccu[1]).to be_a Regex::CharacterRangeUnit
expect(ccu[1].first).to eq "_".ord
end
it "parses a negated character class" do
regex = Regex.new("[^xyz]")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterClassUnit
ccu = seq_unit[0]
expect(ccu.negate).to be_truthy
expect(ccu.size).to eq 3
expect(ccu[0]).to be_a Regex::CharacterRangeUnit
expect(ccu[0].first).to eq "x".ord
end
it "parses - as a plain character at beginning of a character class" do
regex = Regex.new("[-9]")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterClassUnit
ccu = seq_unit[0]
expect(ccu.size).to eq 2
expect(ccu[0]).to be_a Regex::CharacterRangeUnit
expect(ccu[0].first).to eq "-".ord
end
it "parses - as a plain character at end of a character class" do
regex = Regex.new("[0-]")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterClassUnit
ccu = seq_unit[0]
expect(ccu.size).to eq 2
expect(ccu[0]).to be_a Regex::CharacterRangeUnit
expect(ccu[0].first).to eq "0".ord
expect(ccu[1]).to be_a Regex::CharacterRangeUnit
expect(ccu[1].first).to eq "-".ord
end
it "parses - as a plain character at beginning of a negated character class" do
regex = Regex.new("[^-9]")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterClassUnit
ccu = seq_unit[0]
expect(ccu.negate).to be_truthy
expect(ccu.size).to eq 2
expect(ccu[0]).to be_a Regex::CharacterRangeUnit
expect(ccu[0].first).to eq "-".ord
end
it "parses . as a plain character in a character class" do
regex = Regex.new("[.]")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterClassUnit
ccu = seq_unit[0]
expect(ccu.negate).to be_falsey
expect(ccu.size).to eq 1
expect(ccu[0]).to be_a Regex::CharacterRangeUnit
expect(ccu[0].first).to eq ".".ord
end
it "parses - as a plain character when escaped in middle of character class" do
regex = Regex.new("[0\\-9]")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
seq_unit = regex.unit.alternates[0]
expect(seq_unit.size).to eq 1
expect(seq_unit[0]).to be_a Regex::CharacterClassUnit
ccu = seq_unit[0]
expect(ccu.negate).to be_falsey
expect(ccu.size).to eq 3
expect(ccu[0]).to be_a Regex::CharacterRangeUnit
expect(ccu[0].first).to eq "0".ord
expect(ccu[1]).to be_a Regex::CharacterRangeUnit
expect(ccu[1].first).to eq "-".ord
expect(ccu[2]).to be_a Regex::CharacterRangeUnit
expect(ccu[2].first).to eq "9".ord
end
it "parses alternates" do
regex = Regex.new("ab|c")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 2
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[1]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[0].size).to eq 2
expect(regex.unit.alternates[1].size).to eq 1
end
it "parses a ." do
regex = Regex.new("a.b")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 1
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[0][0]).to be_a Regex::CharacterRangeUnit
expect(regex.unit.alternates[0][1]).to be_a Regex::CharacterClassUnit
expect(regex.unit.alternates[0][1].units.size).to eq 2
expect(regex.unit.alternates[0][2]).to be_a Regex::CharacterRangeUnit
end
it "parses something complex" do
regex = Regex.new("(a|)*|[^^]|\\|v|[x-y]+")
expect(regex.unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates.size).to eq 4
expect(regex.unit.alternates[0]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[0].size).to eq 1
expect(regex.unit.alternates[0][0]).to be_a Regex::MultiplicityUnit
expect(regex.unit.alternates[0][0].min_count).to eq 0
expect(regex.unit.alternates[0][0].max_count).to be_nil
expect(regex.unit.alternates[0][0].unit).to be_a Regex::AlternatesUnit
expect(regex.unit.alternates[0][0].unit.alternates.size).to eq 2
expect(regex.unit.alternates[0][0].unit.alternates[0]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[0][0].unit.alternates[0].size).to eq 1
expect(regex.unit.alternates[0][0].unit.alternates[0][0]).to be_a Regex::CharacterRangeUnit
expect(regex.unit.alternates[0][0].unit.alternates[1]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[0][0].unit.alternates[1].size).to eq 0
expect(regex.unit.alternates[1]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[1].size).to eq 1
expect(regex.unit.alternates[1][0]).to be_a Regex::CharacterClassUnit
expect(regex.unit.alternates[1][0].negate).to be_truthy
expect(regex.unit.alternates[1][0].size).to eq 1
expect(regex.unit.alternates[1][0][0]).to be_a Regex::CharacterRangeUnit
expect(regex.unit.alternates[2]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[2].size).to eq 2
expect(regex.unit.alternates[2][0]).to be_a Regex::CharacterRangeUnit
expect(regex.unit.alternates[2][0].first).to eq "|".ord
expect(regex.unit.alternates[2][1]).to be_a Regex::CharacterRangeUnit
expect(regex.unit.alternates[2][1].first).to eq "v".ord
expect(regex.unit.alternates[3]).to be_a Regex::SequenceUnit
expect(regex.unit.alternates[3].size).to eq 1
expect(regex.unit.alternates[3][0]).to be_a Regex::MultiplicityUnit
expect(regex.unit.alternates[3][0].min_count).to eq 1
expect(regex.unit.alternates[3][0].max_count).to be_nil
expect(regex.unit.alternates[3][0].unit).to be_a Regex::CharacterClassUnit
expect(regex.unit.alternates[3][0].unit.size).to eq 1
expect(regex.unit.alternates[3][0].unit[0]).to be_a Regex::CharacterRangeUnit
expect(regex.unit.alternates[3][0].unit[0].first).to eq "x".ord
expect(regex.unit.alternates[3][0].unit[0].last).to eq "y".ord
end
end
end
+344
View File
@@ -0,0 +1,344 @@
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(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_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
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
FileUtils.rm_rf("spec/run")
FileUtils.mkdir_p("spec/run")
end
it "generates a D lexer" do
write_grammar <<EOF
token int /\\d+/;
token plus /\\+/;
token times /\\*/;
drop /\\s+/;
Start -> Foo;
Foo -> int <<
>>
Foo -> plus <<
>>
EOF
build_parser
compile("spec/test_d_lexer.d")
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/;
Start -> E;
E -> E times B;
E -> E plus B;
E -> B;
B -> zero;
B -> one;
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;
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;
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
+11
View File
@@ -0,0 +1,11 @@
require "bundler/setup"
require "propane"
RSpec.configure do |config|
# Enable flags like --only-failures and --next-failure
config.example_status_persistence_file_path = ".rspec_status"
config.expect_with :rspec do |c|
c.syntax = :expect
end
end
+52
View File
@@ -0,0 +1,52 @@
import testparser;
import std.stdio;
int main()
{
return 0;
}
unittest
{
alias Result = Testparser.Decoder.Result;
Result result;
result = Testparser.Decoder.decode_code_point("5");
assert(result == Result.success('5', 1u));
result = Testparser.Decoder.decode_code_point("");
assert(result == Result.eof());
result = Testparser.Decoder.decode_code_point("\xC2\xA9");
assert(result == Result.success(0xA9u, 2u));
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 Result = Testparser.Lexer.Result;
string input = "5 + 4 * \n677 + 567";
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("");
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");
}
-14
View File
@@ -1,14 +0,0 @@
all:
for d in *; do \
if [ -d $$d ]; then \
make -C $$d; \
fi; \
done
clean:
for d in *; do \
if [ -d $$d ]; then \
make -C $$d clean; \
fi; \
done
-15
View File
@@ -1,15 +0,0 @@
TARGET := test
I_SOURCE := itest
CXXFLAGS := -O2
LDFLAGS := -lpcre
all: $(TARGET)
./$(TARGET)
$(TARGET): $(shell which imbecile) $(I_SOURCE).I $(wildcard *.cc)
imbecile $(I_SOURCE).I
$(CXX) -o $@ *.cc $(LDFLAGS)
clean:
-rm -f $(TARGET) *.o $(I_SOURCE).cc $(I_SOURCE).h
-37
View File
@@ -1,37 +0,0 @@
[tokens]
AND and
OR or
NOT not
LPAREN \(
RPAREN \)
WS \s+
EQUALS = %{ cout << "Saw '='" << endl; %}
IDENTIFIER [a-zA-Z_][a-zA-Z_0-9]* %{
cout << "Identify: '" << matches[0] << "'" << endl;
%}
DEC_INT [1-9]\d*\b
${
uint64_t value;
$}
%{
sscanf(matches[0].c_str(), "%lld", &value);
cout << "value: " << value << endl;
%}
HEX_INT 0x([0-9a-fA-F]+)\b ${ uint64_t value; $} %{
sscanf(matches[1].c_str(), "%llx", &value);
cout << "value: " << value << endl;
%}
OCT_INT 0([0-7]*)\b
BIN_INT 0b([01]+)\b
[rules]
Assignment := IDENTIFIER ASSIGN Expression
Expression := IDENTIFIER \
| Assignment
-17
View File
@@ -1,17 +0,0 @@
#include <sstream>
#include <string>
#include "itest.h"
using namespace std;
int main(int argc, char * argv[])
{
Parser p;
stringstream t(string(
"hi there (one and two and three and four) or (two = nine)\n"
"0x42 12345 0 011 0b0011\n"
));
p.parse(t);
}
-202
View File
@@ -1,202 +0,0 @@
#include <string.h> /* memcpy() */
#include <pcre.h>
#include <iostream>
#include <vector>
#include {%header_name%}
using namespace std;
#ifdef I_NAMESPACE
namespace I_NAMESPACE {
#endif
I_CLASSNAME::I_CLASSNAME()
: m_errstr(NULL)
{
}
static TokenRef buildToken(int typeindex)
{
TokenRef token;
switch (typeindex)
{
{%buildToken%}
}
if (!token.isNull())
{
token->setType(typeindex);
}
return token;
}
static void read_istream(istream & i, vector<char> & buff, int & size)
{
size = 0;
int bytes_read;
char read_buff[1000];
while (!i.eof())
{
i.read(&read_buff[0], sizeof(read_buff));
bytes_read = i.gcount();
size += bytes_read;
for (int j = 0; j < bytes_read; j++)
buff.push_back(read_buff[j]);
}
}
bool I_CLASSNAME::parse(istream & i)
{
struct {
const char * name;
const char * definition;
bool process;
pcre * re;
pcre_extra * re_extra;
} tokens[] = {
{%token_list%}
};
if (sizeof(tokens)/sizeof(tokens[0]) == 0)
{
m_errstr = "No tokens defined";
return false;
}
vector<char> buff;
int buff_size;
read_istream(i, buff, buff_size);
if (buff_size <= 0)
{
m_errstr = "0-length input string";
return false;
}
/* append trailing NUL byte for pcre functions */
buff.push_back('\0');
/* compile all token regular expressions */
for (int i = 0; i < sizeof(tokens)/sizeof(tokens[0]); i++)
{
const char * errptr;
int erroffset;
tokens[i].re = pcre_compile(tokens[i].definition, 0,
&errptr, &erroffset, NULL);
if (tokens[i].re == NULL)
{
cerr << "Error compiling token '" << tokens[i].name
<< "' regular expression at position " << erroffset
<< ": " << errptr << endl;
m_errstr = "Error in token regular expression";
return false;
}
tokens[i].re_extra = pcre_study(tokens[i].re, 0, &errptr);
}
int buff_pos = 0;
const int ovector_num_matches = 16;
const int ovector_size = 3 * (ovector_num_matches + 1);
int ovector[ovector_size];
while (buff_pos < buff_size)
{
int longest_match_length = 0;
int longest_match_index = -1;
int longest_match_ovector[ovector_size];
for (int i = 0; i < sizeof(tokens)/sizeof(tokens[0]); i++)
{
int rc = pcre_exec(tokens[i].re, tokens[i].re_extra,
&buff[0], buff_size, buff_pos,
PCRE_ANCHORED | PCRE_NOTEMPTY,
ovector, ovector_size);
if (rc > 0)
{
/* this pattern matched some of the input */
int len = ovector[1] - ovector[0];
if (len > longest_match_length)
{
longest_match_length = len;
longest_match_index = i;
memcpy(longest_match_ovector, ovector, sizeof(ovector));
}
}
}
if (longest_match_index < 0)
{
/* no pattern matched the input at the current position */
cerr << "Parse error" << endl;
return false;
}
Matches matches(tokens[longest_match_index].re,
&buff[0], longest_match_ovector, ovector_size);
TokenRef token = buildToken(longest_match_index);
if (token.isNull())
{
cerr << "Internal Error: null token" << endl;
return false;
}
token->process(matches);
m_tokens.push_back(token);
buff_pos += longest_match_length;
}
}
refptr<Node> Node::operator[](int index)
{
return (0 <= index && index < m_indexed_children.size())
? m_indexed_children[index]
: NULL;
}
refptr<Node> Node::operator[](const std::string & index)
{
return (m_named_children.find(index) != m_named_children.end())
? m_named_children[index]
: NULL;
}
void Token::process(const Matches & matches)
{
{%token_code%}
}
Matches::Matches(pcre * re, const char * data, int * ovector, int ovec_size)
: m_re(re), m_data(data), m_ovector(ovector), m_ovec_size(ovec_size)
{
}
std::string Matches::operator[](int index) const
{
if (0 <= index && index < (m_ovec_size / 3))
{
int idx = 2 * index;
if (m_ovector[idx] >= 0 && m_ovector[idx + 1] >= 0)
{
return string(m_data, m_ovector[idx],
m_ovector[idx + 1] - m_ovector[idx]);
}
}
return "";
}
std::string Matches::operator[](const std::string & index) const
{
int idx = pcre_get_stringnumber(m_re, index.c_str());
if (idx > 0 && idx < (m_ovec_size / 3))
{
if (m_ovector[idx] >= 0 && m_ovector[idx + 1] >= 0)
{
return string(m_data, m_ovector[idx],
m_ovector[idx + 1] - m_ovector[idx]);
}
}
return "";
}
{%token_classes_code%}
#ifdef I_NAMESPACE
};
#endif
-181
View File
@@ -1,181 +0,0 @@
#ifndef IMBECILE_PARSER_HEADER
#define IMBECILE_PARSER_HEADER
#include <pcre.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <iostream>
#include <map>
#include <vector>
#include <list>
{%user_includes%}
{%defines%}
#ifdef I_NAMESPACE
namespace I_NAMESPACE {
#endif
#ifndef REFPTR_H
#define REFPTR_H REFPTR_H
/* Author: Josh Holtrop
* Purpose: Provide a reference-counting pointer-like first order
* C++ object that will free the object it is pointing to when
* all references to it have been destroyed.
* This implementation does not solve the circular reference problem.
* I was not concerned with that when developing this class.
*/
#include <stdlib.h> /* NULL */
template <typename T>
class refptr
{
public:
refptr<T>();
refptr<T>(T * ptr);
refptr<T>(const refptr<T> & orig);
refptr<T> & operator=(const refptr<T> & orig);
refptr<T> & operator=(T * ptr);
~refptr<T>();
T & operator*() const { return *m_ptr; }
T * operator->() const { return m_ptr; }
bool isNull() const { return m_ptr == NULL; }
private:
void cloneFrom(const refptr<T> & orig);
void destroy();
T * m_ptr;
int * m_refCount;
};
template <typename T> refptr<T>::refptr()
{
m_ptr = NULL;
m_refCount = NULL;
}
template <typename T> refptr<T>::refptr(T * ptr)
{
m_ptr = ptr;
m_refCount = new int;
*m_refCount = 1;
}
template <typename T> refptr<T>::refptr(const refptr<T> & orig)
{
cloneFrom(orig);
}
template <typename T> refptr<T> & refptr<T>::operator=(const refptr<T> & orig)
{
destroy();
cloneFrom(orig);
return *this;
}
template <typename T> refptr<T> & refptr<T>::operator=(T * ptr)
{
destroy();
m_ptr = ptr;
m_refCount = new int;
*m_refCount = 1;
return *this;
}
template <typename T> void refptr<T>::cloneFrom(const refptr<T> & orig)
{
this->m_ptr = orig.m_ptr;
this->m_refCount = orig.m_refCount;
if (m_refCount != NULL)
(*m_refCount)++;
}
template <typename T> refptr<T>::~refptr()
{
destroy();
}
template <typename T> void refptr<T>::destroy()
{
if (m_refCount != NULL)
{
if (*m_refCount <= 1)
{
delete m_ptr;
delete m_refCount;
}
else
{
(*m_refCount)--;
}
}
}
#endif
class Matches
{
public:
Matches(pcre * re, const char * data, int * ovector, int ovec_size);
std::string operator[](int index) const;
std::string operator[](const std::string & index) const;
protected:
pcre * m_re;
const char * m_data;
int * m_ovector;
int m_ovec_size;
};
class Node
{
public:
refptr<Node> operator[](int index);
refptr<Node> operator[](const std::string & index);
protected:
std::map< std::string, refptr<Node> > m_named_children;
std::vector< refptr<Node> > m_indexed_children;
};
typedef refptr<Node> NodeRef;
class Token : public Node
{
public:
virtual void process(const Matches & matches);
void setType(int type) { m_type = type; }
int getType() const { return m_type; }
protected:
int m_type;
{%token_data%}
};
typedef refptr<Token> TokenRef;
{%token_classes%}
class I_CLASSNAME
{
public:
I_CLASSNAME();
bool parse(std::istream & in);
const char * getError() { return m_errstr; }
protected:
const char * m_errstr;
std::list<TokenRef> m_tokens;
};
#ifdef I_NAMESPACE
};
#endif
#endif /* IMBECILE_PARSER_HEADER */