]> git.sur5r.net Git - i3/i3/blob - generate-command-parser.pl
Merge branch 'master' into next
[i3/i3] / generate-command-parser.pl
1 #!/usr/bin/env perl
2 # vim:ts=4:sw=4:expandtab
3 #
4 # i3 - an improved dynamic tiling window manager
5 # © 2009-2012 Michael Stapelberg and contributors (see also: LICENSE)
6 #
7 # generate-command-parser.pl: script to generate parts of the command parser
8 # from its specification file parser-specs/commands.spec.
9 #
10 # Requires only perl >= 5.10, no modules.
11
12 use strict;
13 use warnings;
14 use Data::Dumper;
15 use v5.10;
16
17 # reads in a whole file
18 sub slurp {
19     open my $fh, '<', shift;
20     local $/;
21     <$fh>;
22 }
23
24 # Stores the different states.
25 my %states;
26
27 # XXX: don’t hardcode input and output
28 my $input = '../parser-specs/commands.spec';
29 my @raw_lines = split("\n", slurp($input));
30 my @lines;
31
32 # XXX: In the future, we might switch to a different way of parsing this. The
33 # parser is in many ways not good — one obvious one is that it is hand-crafted
34 # without a good reason, also it preprocesses lines and forgets about line
35 # numbers. Luckily, this is just an implementation detail and the specification
36 # for the i3 command parser is in-tree (not user input).
37 # -- michael, 2012-01-12
38
39 # First step of preprocessing:
40 # Join token definitions which are spread over multiple lines.
41 for my $line (@raw_lines) {
42     next if $line =~ /^\s*#/ || $line =~ /^\s*$/;
43
44     if ($line =~ /^\s+->/) {
45         # This is a continued token definition, append this line to the
46         # previous one.
47         $lines[$#lines] = $lines[$#lines] . $line;
48     } else {
49         push @lines, $line;
50         next;
51     }
52 }
53
54 # First step: We build up the data structure containing all states and their
55 # token rules.
56
57 my $current_state;
58
59 for my $line (@lines) {
60     if (my ($state) = ($line =~ /^state ([A-Z_]+):$/)) {
61         #say "got a new state: $state";
62         $current_state = $state;
63     } else {
64         # Must be a token definition:
65         # [identifier = ] <tokens> -> <action>
66         #say "token definition: $line";
67
68         my ($identifier, $tokens, $action) =
69             ($line =~ /
70                 ^\s*                  # skip leading whitespace
71                 ([a-z_]+ \s* = \s*|)  # optional identifier
72                 (.*?) -> \s*          # token 
73                 (.*)                  # optional action
74              /x);
75
76         # Cleanup the identifier (if any).
77         $identifier =~ s/^\s*(\S+)\s*=\s*$/$1/g;
78
79         # Cleanup the tokens (remove whitespace).
80         $tokens =~ s/\s*//g;
81
82         # The default action is to stay in the current state.
83         $action = $current_state if length($action) == 0;
84
85         #say "identifier = *$identifier*, token = *$tokens*, action = *$action*";
86         for my $token (split(',', $tokens)) {
87             my $store_token = {
88                 token => $token,
89                 identifier => $identifier,
90                 next_state => $action,
91             };
92             if (exists $states{$current_state}) {
93                 push @{$states{$current_state}}, $store_token;
94             } else {
95                 $states{$current_state} = [ $store_token ];
96             }
97         }
98     }
99 }
100
101 # Second step: Generate the enum values for all states.
102
103 # It is important to keep the order the same, so we store the keys once.
104 my @keys = keys %states;
105
106 open(my $enumfh, '>', 'GENERATED_enums.h');
107
108 # XXX: we might want to have a way to do this without a trailing comma, but gcc
109 # seems to eat it.
110 say $enumfh 'typedef enum {';
111 my $cnt = 0;
112 for my $state (@keys, '__CALL') {
113     say $enumfh "    $state = $cnt,";
114     $cnt++;
115 }
116 say $enumfh '} cmdp_state;';
117 close($enumfh);
118
119 # Third step: Generate the call function.
120 open(my $callfh, '>', 'GENERATED_call.h');
121 say $callfh 'static char *GENERATED_call(const int call_identifier) {';
122 say $callfh '    char *output = NULL;';
123 say $callfh '    switch (call_identifier) {';
124 my $call_id = 0;
125 for my $state (@keys) {
126     my $tokens = $states{$state};
127     for my $token (@$tokens) {
128         next unless $token->{next_state} =~ /^call /;
129         my ($cmd) = ($token->{next_state} =~ /^call (.*)/);
130         my ($next_state) = ($cmd =~ /; ([A-Z_]+)$/);
131         $cmd =~ s/; ([A-Z_]+)$//;
132         # Go back to the INITIAL state unless told otherwise.
133         $next_state ||= 'INITIAL';
134         my $fmt = $cmd;
135         # Replace the references to identified literals (like $workspace) with
136         # calls to get_string().
137         $cmd =~ s/\$([a-z_]+)/get_string("$1")/g;
138         # Used only for debugging/testing.
139         $fmt =~ s/\$([a-z_]+)/%s/g;
140         $fmt =~ s/"([a-z0-9_]+)"/%s/g;
141
142         say $callfh "         case $call_id:";
143         say $callfh '#ifndef TEST_PARSER';
144         my $real_cmd = $cmd;
145         if ($real_cmd =~ /\(\)/) {
146             $real_cmd =~ s/\(/(&current_match/;
147         } else {
148             $real_cmd =~ s/\(/(&current_match, /;
149         }
150         say $callfh "             output = $real_cmd;";
151         say $callfh '#else';
152         # debug
153         $cmd =~ s/[^(]+\(//;
154         $cmd =~ s/\)$//;
155         $cmd = ", $cmd" if length($cmd) > 0;
156         say $callfh qq|           printf("$fmt\\n"$cmd);|;
157         say $callfh '#endif';
158         say $callfh "             state = $next_state;";
159         say $callfh "             break;";
160         $token->{next_state} = "call $call_id";
161         $call_id++;
162     }
163 }
164 say $callfh '        default:';
165 say $callfh '            printf("BUG in the parser. state = %d\n", call_identifier);';
166 say $callfh '    }';
167 say $callfh '    return output;';
168 say $callfh '}';
169 close($callfh);
170
171 # Fourth step: Generate the token datastructures.
172
173 open(my $tokfh, '>', 'GENERATED_tokens.h');
174
175 for my $state (@keys) {
176     my $tokens = $states{$state};
177     say $tokfh 'cmdp_token tokens_' . $state . '[' . scalar @$tokens . '] = {';
178     for my $token (@$tokens) {
179         my $call_identifier = 0;
180         my $token_name = $token->{token};
181         if ($token_name =~ /^'/) {
182             # To make the C code simpler, we leave out the trailing single
183             # quote of the literal. We can do strdup(literal + 1); then :).
184             $token_name =~ s/'$//;
185         }
186         my $next_state = $token->{next_state};
187         if ($next_state =~ /^call /) {
188             ($call_identifier) = ($next_state =~ /^call ([0-9]+)$/);
189             $next_state = '__CALL';
190         }
191         my $identifier = $token->{identifier};
192         say $tokfh qq|    { "$token_name", "$identifier", $next_state, { $call_identifier } }, |;
193     }
194     say $tokfh '};';
195 }
196
197 say $tokfh 'cmdp_token_ptr tokens[' . scalar @keys . '] = {';
198 for my $state (@keys) {
199     my $tokens = $states{$state};
200     say $tokfh '    { tokens_' . $state . ', ' . scalar @$tokens . ' },';
201 }
202 say $tokfh '};';
203
204 close($tokfh);