#!/usr/bin/tclsh # $Id: taccle.tcl,v 1.6 2005/03/17 20:42:21 tang Exp $ set TACCLE_VERSION 1.2 #//# # Taccle is another compiler compiler written in pure Tcl. reads a # taccle specification file to generate pure Tcl code that # implements an LALR(1) parser. See the {@link README} file for # complete instructions. Additional information may be found at # {@link http://mini.net/tcl/taccle}. # # @author Jason Tang (tang@jtang.org) #//# # Process a definition on a single line, either a literal block or a # % directive. # # @param line text of a definition proc handle_defs {line} { # trim whitespace and remove any comments set line [strip_comments [string trim $line]] if {$line == ""} { return } if {$line == "%\{"} { handle_literal_block } else { # extract the keyword to the left of the first space and the # arguments (if any) to the right if {[regexp -line {^(\S+)\s+(.*)} $line foo keyword args] == 0} { set keyword $line set args "" } switch -- $keyword { "%token" { foreach token_name [split $args] { if {$token_name != ""} { # add the terminal token to the table add_token $token_name $::TERMINAL 0 0 nonassoc } } } "%left" - "%right" - "%nonassoc" { handle_precedence $::next_precedence [string range $keyword 1 end] $args incr ::next_precedence } "%start" { if {$args == ""} { taccle_error "Must supply a token with %start" $::PARAM_ERROR } set ::start_symbol $args } default { taccle_error "Unknown declaration \"$keyword\"" $::SYNTAX_ERROR } } } } # Start reading from the source file and copy everything between ^%\{$ # to ^%\}$ to the destination file. proc handle_literal_block {} { set end_defs 0 set lines_in_block 0 while {$end_defs == 0} { if {[gets $::src line] < 0} { taccle_error "No terminator to verbatim section found " $::SYNTAX_ERROR } elseif {[string trim $line] == "%\}"} { set end_defs 1 } else { puts $::dest $line } incr lines_in_block } incr ::line_count $lines_in_block } # Assigns operator precedence to each token in $tokens. Adds the # token as a TERMINAL to the token table. # # @param level integer value for token precedence # @param direction direction of precedence, either left, # right, or nonassoc # @param tokens list of terminals to which assign precedence proc handle_precedence {level direction tokens} { foreach token $tokens { if {[regexp -- {\A\'(.)\'\Z} $token foo c]} { add_token $c $::TERMINAL 1 $level $direction } else { add_token $token $::TERMINAL 0 $level $direction } } } # The nine steps to actually building a parser, given a string buffer # containing all of the rules. # # @param rules_buf a very large string consisting of all of the # grammar's rules proc build_parser {rules_buf} { # setp 0: parse the entire rules buffer into separate productions handle_rules_buf $rules_buf # step 1: rewrite the grammar, then augment it rewrite_grammar # step 2: determine which non-terminals are nullable generate_nullable_table # step 3: generate FIRST table for each element in the token table generate_first_table # step 4: now generate FOLLOW table for each element generate_follow_table # step 5: build canonical LR(1) table generate_lr1 # step 6: combine cores into LALR(1) table generate_lalr1 # step 7: wherever there exists a shift/reduce conflict, choose to # reduce wherever the precedence table dictates such resolve_precedences # step 8: check for infinite recursions check_recursions # step 9: finally take LALR(1) table and generate a state # transition matrix generate_lalr1_parse_table } # Parses the rules buffer, extracting each rule and adding # pseudo-rules wherever embedded actions exist. # # @param rules_buf remaining rules to handle proc handle_rules_buf {rules_buf} { # counts number of rules in the grammar # rule number 0 is reserved for the special augmentation S' -> S set ::rule_count 1 set prev_lhs "" # keep track of pseudo-rules (used for embedded actions) set pseudo_count 1 # add the special end marker set ::token_table("\$",t) $::TERMINAL set ::token_table("\$") 0 set ::token_id_table(0) "\$" set ::token_id_table(0,t) $::TERMINAL set ::prec_table(0) 0 set ::prec_table(0,dir) nonassoc # add the special error token add_token error $::TERMINAL 1 0 nonassoc while {[string length $rules_buf] > 0} { # consume blank lines if {[regexp -line -- {\A([[:blank:]]*\n)} $rules_buf foo blanks]} { set rules_buf [string range $rules_buf [string length $blanks] end] incr ::line_count continue } # extract left hand side if {[regexp -line -- {\A\s*(\w+)\s*:} $rules_buf foo lhs]} { add_token $lhs $::NONTERMINAL 0 0 nonassoc set prev_lhs $lhs } elseif {[regexp -line -- {\A\s*\|} $rules_buf foo]} { if {$prev_lhs == ""} { taccle_error "No previously declared left hand side" $::SYNTAX_ERROR } set lhs $prev_lhs } elseif {[regexp -line -- {\A\s*\Z} $rules_buf]} { # only whitespace left break } else { taccle_error "No left hand side found" $::SYNTAX_ERROR } set rules_buf [string range $rules_buf [string length $foo] end] # read the rule derivation, which is everything up to a bar or # semicolon set rhs "" set action "" set done_deriv 0 set num_lines 0 while {$rules_buf != "" && $done_deriv != 1} { switch -- [string index $rules_buf 0] { | { set done_deriv 1 } ; { set done_deriv 1 set prev_lhs "" set rules_buf [string range $rules_buf 1 end] } "\n" { incr num_lines append rhs " " set rules_buf [string range $rules_buf 1 end] } ' { append rhs [string range $rules_buf 0 2] set rules_buf [string range $rules_buf 3 end] } \{ { # keep scanning until end of action found set a "" set rp 1 set found_end 0 while {!$found_end && $rp < [string length $rules_buf]} { set c [string index $rules_buf $rp] if {$c == "\}"} { if {[info complete $a]} { set found_end 1 } else { append a "\}" } } elseif {$c == "\n"} { append a $c incr num_lines } else { append a $c } incr rp } if {!$found_end} { taccle_error "Unmatched `\{'" $::SYNTAX_ERROR } set action $a set rules_buf [string range $rules_buf $rp end] } default { set c [string index $rules_buf 0] if {$action != "" && ![string is space $c]} { # embedded action found; add a special rule for it set pseudo_name "@PSEUDO$pseudo_count" add_token $pseudo_name $::NONTERMINAL 0 0 nonassoc set ::rule_table($::rule_count,l) $pseudo_name set ::rule_table($::rule_count,d) "" set ::rule_table($::rule_count,dc) 0 set ::rule_table($::rule_count,a) $action set ::rule_table($::rule_count,e) 0 set ::rule_table($::rule_count,line) $::line_count append rhs "$pseudo_name " set action "" incr pseudo_count incr ::rule_count } else { append rhs $c set rules_buf [string range $rules_buf 1 end] } } } } if {$rules_buf == "" && $done_deriv == 0} { taccle_error "Rule does not terminate" $::SYNTAX_ERROR } set derivation [string trim $rhs] set deriv_list "" set deriv_count 0 set prec_next 0 foreach token [split $derivation] { if {$prec_next} { # check that argument to %prec is a terminal symbol if {![info exists ::token_table($token)] || \ $::token_table($token,t) != $::TERMINAL} { taccle_error "Argument to %prec is not a terminal symbol" $::GRAMMAR_ERROR } set ::rule_table($::rule_count,prec) $::token_table($token) set prec_next 0 continue } if {$token == "%prec"} { set prec_next 1 continue } if {[regexp -- {\A\'(.)\'\Z} $token foo c]} { add_token $c $::TERMINAL 1 0 nonassoc set token $c } if {$token != ""} { if {[string range $token 0 6] == "@PSEUDO"} { set ::rule_table([expr {$::rule_count - 1}],e) $deriv_count } lappend deriv_list $token incr deriv_count } } if {$prec_next} { taccle_error "%prec modifier has no associated terminal symbol" $::PARAM_ERROR } incr ::line_count $num_lines set ::rule_table($::rule_count,l) $lhs set ::rule_table($::rule_count,d) $deriv_list set ::rule_table($::rule_count,dc) [llength $deriv_list] set ::rule_table($::rule_count,a) $action set ::rule_table($::rule_count,line) $::line_count incr ::rule_count } } # Post-process the grammar by augmenting it and and replacing all # tokens with their id values. proc rewrite_grammar {} { set ::rule_table(0,l) "start'" if {[info exists ::start_symbol]} { if {![info exists ::token_table($::start_symbol)]} { taccle_error "Token given by %start does not exist" $::PARAM_ERROR } if {$::token_table($::start_symbol,t) == $::TERMINAL} { taccle_error "Token given by %start is a terminal." $::PARAM_ERROR } set ::rule_table(0,d) $::start_symbol } else { set ::rule_table(0,d) $::rule_table(1,l) } set ::rule_table(0,dc) 1 set ::rule_table(0,prec) 0 set ::start_token_id [add_token "start'" $::NONTERMINAL 0 0 nonassoc] set ::token_list [lsort -command tokid_compare $::token_list] # now go through grammar and replace all token names with their id # number for {set i 0} {$i < $::rule_count} {incr i} { set ::rule_table($i,l) $::token_table($::rule_table($i,l)) set new_deriv_list "" foreach deriv $::rule_table($i,d) { if {![info exists ::token_table($deriv)]} { taccle_error "Symbol $deriv used, but is not defined as a token and has no rules." $::GRAMMAR_ERROR } lappend new_deriv_list $::token_table($deriv) } set ::rule_table($i,d) $new_deriv_list # set the rule's precedence only if it was not already specified if {![info exist ::rule_table($i,prec)]} { set ::rule_table($i,prec) [get_prec $new_deriv_list] } } # check for unused tokens set used_list [concat "error" [recurse_dfs $::start_token_id ""]] foreach tok_id $::token_list { if {[lsearch -exact $used_list $tok_id] == -1} { taccle_warn "Token $::token_id_table($tok_id) unused." } else { lappend ::used_token_list $tok_id } } # add to the used token list {$} but /not/ start' set ::used_token_list [concat [lrange $::used_token_list 0 end-1] \ $::token_table("\$")] } # Determine which non-terminals are nullable. Any terminal which can # be simplified to just an epsilon transition is nullable. proc generate_nullable_table {} { set nullable_found 1 while {$nullable_found} { set nullable_found 0 foreach tok_id $::token_list { if {[info exist ::nullable_table($tok_id)]} { continue } if {$::token_id_table($tok_id,t) == $::TERMINAL} { set ::nullable_table($tok_id) 0 continue } for {set i 0} {$i < $::rule_count} {incr i} { set lhs $::rule_table($i,l) if {$lhs != $tok_id} { continue } set rhs [lindex $::rule_table($i,d) 0] if {$rhs == ""} { set ::nullable_table($lhs) 1 set nullable_found 1 } else { set nullable 0 foreach r $rhs { if {[info exists ::nullable_table($r)]} { set nullable $::nullable_table($r) break } } if {$nullable} { set ::nullable_table($lhs) 1 set nullable_found 1 } } } } } foreach tok_id $::token_list { if {![info exist ::nullable_table($tok_id)]} { set ::nullable_table($tok_id) 0 } } } # Generate the table of FIRST symbols for the grammar. proc generate_first_table {} { foreach tok_id $::token_list { generate_first_recurse $tok_id "" } } # Recursively calculates the FIRST set for a given token, handling # nullable terminals as well. # # @param tok_id id of token to generate FIRST set # @param history list of tokens already examined # @return list of tokens (including -1 for epsilon) in tok_id's FIRST set proc generate_first_recurse {tok_id history} { if {[lsearch -exact $history $tok_id] >= 0} { return "" } if {[info exists ::first_table($tok_id)]} { return $::first_table($tok_id) } if {$::token_id_table($tok_id,t) == $::TERMINAL} { set ::first_table($tok_id) $tok_id return $tok_id } # FIRST = union of all first non-terminals on rhs. if a # non-terminal is nullable, then add FIRST of the following # terminal to the FIRST set. keep repeating while nullable. set first_union "" for {set i 0} {$i < $::rule_count} {incr i} { set lhs $::rule_table($i,l) if {$lhs != $tok_id} { continue } if {$::rule_table($i,dc) == 0} { # empty rule, so add the special epsilon marker -1 to the FIRST set lappend first_union -1 } else { foreach r $::rule_table($i,d) { lconcat first_union [generate_first_recurse $r [concat $history $tok_id]] if {$::nullable_table($r) == 0} { break } } } } set ::first_table($tok_id) [lsort -increasing -unique $first_union] return $first_union } # Generate the table of FOLLOW symbols for the grammar. proc generate_follow_table {} { set ::follow_table($::token_table(start')) $::token_table("\$") foreach tok_id $::token_list { generate_follow_recurse $tok_id "" } } # Recursively calculates the FOLLOW set for a given token, handling # nullable terminals as well. # # @param tok_id id of token to generate FOLLOW set # @param history list of tokens already examined # @return list of tokens in tok_id's FOLLOW set proc generate_follow_recurse {tok_id history} { if {[lsearch -exact $history $tok_id] >= 0} { return "" } if {[info exists ::follow_table($tok_id)]} { return $::follow_table($tok_id) } set follow_union "" for {set i 0} {$i < $::rule_count} {incr i} { # if the token is on the rhs of the rule then FOLLOW includes # the FIRST of the token following it; if at end of rule (or # can be derived to end of rule) then FOLLOW includes the # FOLLOW of the lhs set rhs $::rule_table($i,d) for {set j [expr {$::rule_table($i,dc) - 1}]} {$j >= 0} {incr j -1} { set r [lindex $rhs $j] if {$r != $tok_id} { continue } set k [expr {$j + 1}] set gamma [lindex $rhs $k] if {$gamma != ""} { lconcat follow_union [all_but_eps $::first_table($gamma)] } set at_end_of_list 1 while {$k < $::rule_table($i,dc)} { if {![has_eps $::first_table([lindex $rhs $k])]} { set at_end_of_list 0 break } incr k } if {$at_end_of_list} { set lhs $::rule_table($i,l) lconcat follow_union [generate_follow_recurse $lhs [concat $history $tok_id]] } } } set ::follow_table($tok_id) [lsort -increasing -unique $follow_union] return $follow_union } # Construct a canonical LR(1) by taking the start rule (rule 0) and # successively adding closures/states until no more new states. proc generate_lr1 {} { # first add start rule to the closure list set first_item [list [list 0 $::token_table("\$") 0]] set first_closure [add_closure $first_item 0 1] set ::lr1_table(0) [concat $first_item $first_closure] # used to keep count of total number of states produced by LR(1) set ::next_lr1_state 1 # keep generating items until none remain for {set state_pointer 0} {$state_pointer < $::next_lr1_state} {incr state_pointer} { # iterate through each token, adding transitions to new state(s) set trans_list "" set oldclosure_list $::lr1_table($state_pointer) foreach tok_id $::token_list { set todo_list "" set working_list "" foreach item $oldclosure_list { foreach {rule lookahead position} $item {} if {$position >= $::rule_table($rule,dc)} { # at end of rule; don't expand (and remove it # from the list) continue } set nexttoken [lindex $::rule_table($rule,d) $position] if {$nexttoken == $tok_id} { # item's next token matches the one currently # saught; add it to the working list lappend working_list $item } else { # item was not used yet -- add it back to the # todo list lappend todo_list $item } } set oldclosure_list $todo_list if {$working_list != ""} { set new_closure "" foreach item $working_list { # move pointer ahead to the next position foreach {rule lookahead position} $item {} incr position set newitem [list $rule $lookahead $position] lappend new_closure $newitem } set new_closure [concat $new_closure \ [add_closure $new_closure 0 [llength $working_list]]] # add a transition out of this state -- to a # previously examined state if possible, or else # create a new state with my new closure set next_state -1 for {set i 0} {$i < $::next_lr1_state} {incr i} { if {[lsort $::lr1_table($i)] == [lsort $new_closure]} { set next_state $i break } } if {$next_state == -1} { # create a new state set ::lr1_table($::next_lr1_state) $new_closure lappend trans_list [list $tok_id $::next_lr1_state] incr ::next_lr1_state } else { # reuse existing state lappend trans_list [list $tok_id $next_state] } } } set ::lr1_table($state_pointer,trans) [lsort -command tokid_compare -index 0 $trans_list] } } # Successively add closures from LR(1) table to LALR(1) table merging # kernels with similar cores. proc generate_lalr1 {} { for {set i 0} {$i < $::next_lr1_state} {incr i} { # as matching closures are found change their mapping here set state_mapping_table($i) $i } # go through all elements of LR(1) table and generate their cores. # this will make future comparisons easier. for {set i 0} {$i < $::next_lr1_state} {incr i} { set core "" foreach item $::lr1_table($i) { lappend core [list [lindex $item 0] [lindex $item 2]] } set core_table($i) [lsort $core] } lappend new_lalr_states(0) 0 for {set i 1} {$i < $::next_lr1_state} {incr i} { set found_matching 0 for {set j 0} {$j < $i} {incr j} { if {$core_table($i) == $core_table($j)} { # found a matching core -- change its mapping set state_mapping_table($i) $state_mapping_table($j) # because this state is being eliminated, shuffle all # future states down one for {set k [expr {$i + 1}]} {$k < $::next_lr1_state} {incr k} { incr state_mapping_table($k) -1 } # merge state $i into state $j lappend new_lalr_states($j) $i set found_matching 1 break } } if {!$found_matching} { lappend new_lalr_states($i) $i } } # now copy items from LR(1) table to LALR(1) table set ::next_lalr1_state 0 for {set i 0} {$i < $::next_lr1_state} {incr i} { if {![info exists new_lalr_states($i)]} { # state no longer exists (it got merged into another one) continue } # first merge together all lookaheads set ::lalr1_table($::next_lalr1_state) $::lr1_table([lindex $new_lalr_states($i) 0]) foreach state [lrange $new_lalr_states($i) 1 end] { set ::lalr1_table($::next_lalr1_state) \ [merge_closures $::lalr1_table($::next_lalr1_state) $::lr1_table($state)] } # now rewrite the transition table foreach trans $::lr1_table($i,trans) { foreach {symbol new_state} $trans {} lappend ::lalr1_table($::next_lalr1_state,trans) \ [list $symbol $state_mapping_table($new_state)] } incr ::next_lalr1_state } } # Takes the LALR(1) table and resolves precedence issues by removing # transitions whenever the precedence values indicate a reduce instead # of a shift. proc resolve_precedences {} { for {set i 0} {$i < $::next_lalr1_state} {incr i} { # scan through all kernel items that are at the end of their # rule. for those, use the precedence table to decide to keep # a transition (a shift) or not (a reduce) foreach item $::lalr1_table($i) { foreach {rule lookahead position} $item {} if {$position < $::rule_table($rule,dc) || \ ![info exist ::lalr1_table($i,trans)]} { continue } set rule_prec_tok $::rule_table($rule,prec) set rule_prec_level $::prec_table($rule_prec_tok) set rule_prec_dir $::prec_table($rule_prec_tok,dir) set new_trans "" foreach trans $::lalr1_table($i,trans) { set trans_tok [lindex $trans 0] if {[lsearch $lookahead $trans_tok] == -1} { lappend new_trans $trans continue } set trans_tok_level $::prec_table($trans_tok) set trans_tok_dir $::prec_table($trans_tok,dir) if {$rule_prec_dir == "nonassoc" || \ $trans_tok_dir == "nonassoc" || \ $rule_prec_level < $trans_tok_level || \ ($rule_prec_level == $trans_tok_level && $rule_prec_dir == "right")} { # precedence says to shift, so keep this transition lappend new_trans $trans } else { taccle_warn "Conflict in state $i between rule $rule and token \"$trans_tok\", resolved as reduce." } } set ::lalr1_table($i,trans) $new_trans } } } # Check if the grammar contains any infinite recursions. proc check_recursions {} { set cleared "" for {set i 0} {$i < $::next_lalr1_state} {incr i} { if {[lsearch -exact $cleared $i] >= 0} { continue } set cleared [get_cleared $i {} $cleared] } } # Recursively performs a DFS search through the LALR(1) table to check # for cycles. In each node check if the position is at the end of any # rule; this marks the node is "reducible" and it is added to the # 'cleared' list. Otherwise recurse on each terminal transitioning # out of this state. If a state and all of its transitions are not # reducible then abort with an error. # # @param state which state within the LALR(1) table to examine # @param history a list of states so far examined on this pass # @param cleared a list of states which have already been verified as reducible # # @return a new cleared list, or an empty list of this state is not reducible proc get_cleared {state history cleared} { if {[lsearch -exact $cleared $state] >= 0} { return $cleared } if {[lsearch -exact $history $state] >= 0} { return {} } # check if any items in this closure are reducible; if so then # this state passes set token -1 foreach item $::lalr1_table($state) { foreach {rule lookahead position} $item {} if {$position == $::rule_table($rule,dc)} { return [concat $cleared $state] } if {$position == 0} { set token $::rule_table($rule,l) } } # recursively check all terminals transitioning out of this state; # if none of the new states eventually reduce then report this as # a cycle foreach trans $::lalr1_table($state,trans) { foreach {tok_id nextstate} $trans {} if {$::token_id_table($tok_id,t) == $::TERMINAL} { set retval [get_cleared $nextstate [concat $history $state] $cleared] if {[llength $retval] > 0} { return [concat $retval $state] } } } if {$token == -1} { puts stderr "OOPS: should not have gotten here!" exit -1 } set ::line_count $::rule_table($rule,line) taccle_error "Token $::token_id_table($token) appears to recurse infinitely" $::GRAMMAR_ERROR } # Takes the LALR(1) table and generates the LALR(1) transition table. # For terminals do a shift to the new state. For non-terminals reduce # when the next token is a lookahead. Detect shift/reduce conflicts; # resolve by giving precedence to shifting. Detect reduce/reduce # conflicts and resolve by reducing to the first rule found. proc generate_lalr1_parse_table {} { for {set i 0} {$i < $::next_lalr1_state} {incr i} { foreach item $::lalr1_table($i) { foreach {rule lookahead position} $item {} if {$position >= $::rule_table($rule,dc)} { if {$rule == 0} { set command "accept" } else { set command "reduce" } set token_list $lookahead # target for a reduce/accept is which rule to use # while accepting set target $rule } else { set token [lindex $::rule_table($rule,d) $position] if {$::token_id_table($token,t) == $::TERMINAL} { set command "shift" } else { set command "goto" } set token_list [list $token] # target for a shift/goto is the new state to move to set target "" foreach trans $::lalr1_table($i,trans) { foreach {tok_id nextstate} $trans {} if {$tok_id == $token} { set target $nextstate break } } # this token must have been consumed by shift/reduce # conflict resolution through the precedence table # (above) if {$target == ""} { continue } } foreach token $token_list { # check for shift/reduce conflicts if {[info exists ::lalr1_parse($i:$token)] && \ $::lalr1_parse($i:$token) != $command} { # shifting takes precedence, so overwrite table # entry if needed if {$::lalr1_parse($i:$token) == "shift"} { taccle_warn "Shift/Reduce error in state $i, token \"$::token_id_table($token)\", resolved by keeping shift." break } taccle_warn "Shift/Reduce error in state $i between rule $::lalr1_parse($i:$token,target) and token \"$::token_id_table($token)\", resolved as shift." unset ::lalr1_parse($i:$token,target) } set ::lalr1_parse($i:$token) $command # check for reduce/reduce conflicts # (theoretically it is impossible to have a shift/shift error) if {[info exists ::lalr1_parse($i:$token,target)] && \ $::lalr1_parse($i:$token,target) != $target} { taccle_warn "Reduce/Reduce error in state $i, token \"$::token_id_table($token)\", resolved by reduce to rule $::lalr1_parse($i:$token,target)." break } set ::lalr1_parse($i:$token,target) $target } } } } ###################################################################### # utility routines that actually handle writing parser to output files # Writes to the destination file utility functions called by yyparse # as well as by user-supplied actions. proc write_parser_utils {} { puts $::dest " ###### # Begin autogenerated taccle (version $::TACCLE_VERSION) routines. # Although taccle itself is protected by the GNU Public License (GPL) # all user-supplied functions are protected by their respective # author's license. See http://mini.net/tcl/taccle for other details. ###### namespace eval ${::p} \{ variable yylval {} variable table variable rules variable token {} namespace export yylex \} proc ${::p}::YYABORT \{\} \{ return -code return 1 \} proc ${::p}::YYACCEPT \{\} \{ return -code return 0 \} proc ${::p}::yyclearin \{\} \{ variable token set token {} \} proc ${::p}::yyerror \{s\} \{ puts stderr \$s \} proc ${::p}::setupvalues \{stack pointer numsyms\} \{ upvar 1 1 y set y \{\} for \{set i 1\} \{\$i <= \$numsyms\} \{incr i\} \{ upvar 1 \$i y set y \[lindex \$stack \$pointer\] incr pointer \} \} proc ${::p}::unsetupvalues \{numsyms\} \{ for \{set i 1\} \{\$i <= \$numsyms\} \{incr i\} \{ upvar 1 \$i y unset y \} \}" } # Writes to the destination file the actual parser including LALR(1) # table. proc write_parser {} { write_array $::dest ${::p}::table [array get ::lalr1_parse] write_array $::dest ${::p}::rules [array get ::rule_table *l] write_array $::dest ${::p}::rules [array get ::rule_table *dc] write_array $::dest ${::p}::rules [array get ::rule_table *e] puts $::dest "\nproc ${::p}::yyparse {} { variable yylval variable table variable rules variable token set state_stack {0} set ${::p}value_stack {{}} set token \"\" set ${::p}accepted 0 while {\$${::p}accepted == 0} { set ${::p}state \[lindex \$state_stack end\] if {\$token == \"\"} { set yylval \"\" set token \[yylex\] set ${::p}buflval \$yylval } if {!\[info exists table(\$${::p}state:\$token)\]} { \# pop off states until error token accepted while {\[llength \$state_stack\] > 0 && \\ !\[info exists table(\$${::p}state:error)]} { set state_stack \[lrange \$state_stack 0 end-1\] set ${::p}value_stack \[lrange $${::p}value_stack 0 \\ \[expr {\[llength \$state_stack\] - 1}\]\] set ${::p}state \[lindex \$state_stack end\] } if {\[llength \$state_stack\] == 0} { ${::p}::yyerror \"parse error\" return 1 } lappend state_stack \[set ${::p}state \$table($${::p}state:error,target)\] lappend ${::p}value_stack {} \# consume tokens until it finds an acceptable one while {!\[info exists table(\$${::p}state:\$token)]} { if {\$token == 0} { ${::p}::yyerror \"end of file while recovering from error\" return 1 } set yylval {} set token \[yylex\] set ${::p}buflval \$yylval } continue } switch -- \$table(\$${::p}state:\$token) { shift { lappend state_stack \$table(\$${::p}state:\$token,target) lappend ${::p}value_stack \$${::p}buflval set token \"\" } reduce { set ${::p}rule \$table(\$${::p}state:\$token,target) set ${::p}l \$rules(\$${::p}rule,l) if \{\[info exists rules(\$${::p}rule,e)\]\} \{ set ${::p}dc \$rules(\$${::p}rule,e) \} else \{ set ${::p}dc \$rules(\$${::p}rule,dc) \} set ${::p}stackpointer \[expr {\[llength \$state_stack\]-\$${::p}dc}\] ${::p}::setupvalues \$${::p}value_stack \$${::p}stackpointer \$${::p}dc set _ \$1 set yylval \[lindex \$${::p}value_stack end\] switch -- \$${::p}rule {" for {set i 0} {$i < $::rule_count} {incr i} { if {[info exists ::rule_table($i,a)] && [string trim $::rule_table($i,a)] != ""} { puts $::dest " $i { $::rule_table($i,a) }" } } puts $::dest " } ${::p}::unsetupvalues \$${::p}dc # pop off tokens from the stack if normal rule if \{!\[info exists ::${::p}rules(\$${::p}rule,e)\]\} \{ incr ${::p}stackpointer -1 set state_stack \[lrange \$state_stack 0 \$${::p}stackpointer\] set ${::p}value_stack \[lrange \$${::p}value_stack 0 \$${::p}stackpointer\] \} # now do the goto transition lappend state_stack \$table(\[lindex \$state_stack end\]:\$${::p}l,target) lappend ${::p}value_stack \$_ } accept { set ${::p}accepted 1 } goto - default { puts stderr \"Internal parser error: illegal command \$table(\$${::p}state:\$token)\" return 2 } } } return 0 } ###### # end autogenerated taccle functions ###### " } # Pretty-prints an array to a file descriptor. Code contributed by # jcw. # # @param fd file descriptor to which write the array # @param name name of array to declare within the file # @param values list of 2-ple values proc write_array {fd name values} { puts $fd "\narray set $name {" foreach {x y} $values { puts $fd " [list $x $y]" } puts $fd "}" } # Writes a header file that should be [source]d by the lexer. proc write_header_file {} { # scan through token_table and write out all non-implicit terminals puts $::header "namespace eval ${::p} \{\}" puts $::header "" foreach tok_id $::token_list { if {$::token_id_table($tok_id,t) == $::TERMINAL && \ [string is integer $tok_id] && $tok_id >= 256} { set token $::token_id_table($tok_id) puts $::header "set ${::p}::${token} $tok_id" } } puts $::header "set yylval \{\}" } ###################################################################### # utility functions # Adds a token to the token table, checking that it does not already # exist. Returns the ID for the token (either old one if token # already exists or the newly assigned id value). # # @param token_name name of token to add # @param type type of token, either $::TERMINAL or $::NON_TERMINAL # @param implicit for $::TERMINAL tokens, 1 if the token is implicitly # declared # @param prec_level precedence level for token # @param prec_dir direction of precedence, either left, # right, or nonassoc # @return id value for this token proc add_token {token_name type implicit prec_level prec_dir} { if {$token_name == "\$"} { taccle_error "The token '$' is reserved and may not be used in productions." $::SYNTAX_ERROR } if {$token_name == "\{" || $token_name == 0} { taccle_error "Literal value $token_name not allowed; define a %token instead" $::SYNTAX_ERROR } if [info exists ::token_table($token_name)] { set id $::token_table($token_name) if {$::token_table($token_name,t) == $type} { # token already exists; modify its precedence level if necessary if {$::prec_table($id) < $prec_level} { taccle_warn "Redefining precedence of $token_name" set ::prec_table($id) $prec_level set ::prec_table($id,dir) $prec_dir } set ::token_id_table($id,line) $::line_count return $id } set old_type [expr {$::token_table($token_name,t) == 1 ? "non-" : ""}]terminal taccle_error "Token $token_name already declared as a $old_type" $::GRAMMAR_ERROR } if $implicit { set ::token_table($token_name) $token_name set id $token_name } else { set ::token_table($token_name) $::next_token_id set id $::next_token_id incr ::next_token_id } set ::token_table($token_name,t) $type set ::token_id_table($id) $token_name set ::token_id_table($id,t) $type set ::token_id_table($id,line) $::line_count lappend ::token_list $id set ::prec_table($id) $prec_level set ::prec_table($id,dir) $prec_dir return $id } # Adds closures to each item on $closure_list, starting from the index # $closure_pointer. Keeps adding closures until no more are added. # # @param closure_list list of closures to process # @param closure_pointer index into $closure_list to which start # @param original_length original size of $closure_list # @return list of closures added proc add_closure {closure_list closure_pointer original_length} { set orig_closure_pointer [expr {$closure_pointer + $original_length}] # keep adding items to the closure list until no more while {$closure_pointer < [llength $closure_list]} { set item [lindex $closure_list $closure_pointer] incr closure_pointer foreach {rule lookahead position} $item {} set mylength $::rule_table($rule,dc) if {$position < $mylength} { set nexttoken [lindex $::rule_table($rule,d) $position] if {$::token_id_table($nexttoken,t) == $::TERMINAL} { continue } # the lookahead is the FIRST of the rule /after/ # nexttoken, or the current lookahead if at the end of # rule. if the next token is NULLABLE then the lookahead # includes that which FOLLOWS it set beta_pos [expr {$position + 1}] if {$beta_pos >= $mylength} { set nextfirst $lookahead } else { set n [lindex $::rule_table($rule,d) $beta_pos] set nextfirst [all_but_eps $::first_table($n)] if {$::nullable_table($n)} { set nextfirst [lsort -unique [concat $nextfirst $::follow_table($n)]] } } for {set rule_num 0} {$rule_num < $::rule_count} {incr rule_num} { if {$::rule_table($rule_num,l) != $nexttoken} { continue } set newitem [list $rule_num $nextfirst 0] set closure_list [merge_closures $closure_list [list $newitem]] } } } return [lrange $closure_list $orig_closure_pointer end] } # Recurses through all productions, recording which tokens are # actually used by the grammar. Tokens used to indicate a rule's # precedence are also added. Returns a list of tokens used; note that # this list can (and probably will) include duplicates. # # @param tok_id id of token to start # @param history list of tok_id's already examined # @return list of tokens used proc recurse_dfs {tok_id history} { if {[lsearch -exact $history $tok_id] >= 0} { return $history } if {$::token_id_table($tok_id,t) == $::TERMINAL} { return [concat $history $tok_id] } lappend history $tok_id for {set i 0} {$i < $::rule_count} {incr i} { set lhs $::rule_table($i,l) if {$lhs == $tok_id} { foreach deriv $::rule_table($i,d) { set history [recurse_dfs $deriv $history] } lconcat history $::rule_table($i,prec) } } return $history } # Given a line, returns a new line with any comments removed. # # @param line string with a possible comment # @return line with any commens removed proc strip_comments {line} { regexp -- {\A([^\#]*)} $line foo line return $line } # Combines unique elements of the two closures, also merging lookahead # symbols, and returns the new closure. # # @param closure1 first closure to merge # @param closure2 second closure to merge # @return $closure1 and $closure2 merged together, with duplicated removed proc merge_closures {closure1 closure2} { foreach item2 $closure2 { foreach {rule2 lookahead2 pos2} $item2 {} set found_match 0 for {set i 0} {$i < [llength $closure1]} {incr i} { foreach {rule1 lookahead1 pos1} [lindex $closure1 $i] {} if {$rule2 == $rule1 && $pos2 == $pos1} { set lookahead1 [lsort -uniq [concat $lookahead1 $lookahead2]] lset closure1 $i [list $rule1 $lookahead1 $pos1] set found_match 1 break } } if {!$found_match} { lappend closure1 $item2 } } return $closure1 } # Compares two token id values. If the two are integers then uses # their values for comparison; otherwise performs a string comparison. # Integer values are always "greater than" strings. # # @param a first token id # @param b second token id # @return -1 if a is less than b, 1 if # a is greater, otherwise 0 proc tokid_compare {a b} { if {[string is integer $a] && [string is integer $b]} { if {$a < $b} { return -1 } else { return 1 } } if [string is integer $a] { return 1 } if [string is integer $b] { return -1 } return [string compare $a $b] } # Given a list, returns all everything in it except for any elements # of value "-1", which corresponds with the epsilon symbol. # # @param first_list list of tokens (presumably a FIRST set) # @return new list with all -1 values removed proc all_but_eps {first_list} { set new_list "" foreach tok $first_list { if {$tok != -1} { lappend new_list $tok } } return $new_list } # Returns truth if the element value "-1", corresponding with the # epsilon symbol, resides within the first list $first_list. # # @param first_list list of tokens (presumably a FIRST set) # @return 1 if $first_list has the element -1, 0 otherwise proc has_eps {first_list} { foreach tok $first_list { if {$tok == -1} { return 1 } } return 0 } # Given a list of tokens, returns the token with highest precedence # level. # # @param tok_list list of token ids # @return token with highest precedence; in case of tie returns first # one found proc get_prec {tok_list} { set prec_token 0 foreach tok $tok_list { if {$::prec_table($tok) > $::prec_table($prec_token)} { set prec_token $tok } } return $prec_token } # Appends the first list a flattened version of the second, but only # if the second is non-empty. # # @param list first list # @param lists list of lists to append # @return new list proc lconcat {list lists} { upvar $list l if {$lists != ""} { set l [concat $l $lists] } else { return $l } } # Retrives a parameter from the options list. If no parameter exists # then abort with an error very reminisicent of C's # getopt function; otherwise increment # param_num by one. # # @param param_list list of parameters from the command line # @param param_num index into param_list to retrieve # @param param_name name of the parameter, used when reporting an error # @return the $param_num'th element into $param_list proc get_param {param_list param_num param_name} { upvar $param_num pn incr pn if {$pn >= [llength $param_list]} { puts stderr "taccle: option requires an argument -- $param_name" exit $::PARAM_ERROR } return [lindex $param_list $pn] } # Display to standard error a message, then abort the program. proc taccle_error {message returnvalue} { if {$::verbose != ""} { puts $::verbose "$message (line $::line_count)" } puts stderr "$message (line $::line_count)" exit $returnvalue } # Display a message to standard error if warnings enabled. Write to # the verbose output file if verbose is enabled. proc taccle_warn {message} { if {$::show_warnings} { puts stderr $message } if {$::verbose != ""} { puts $::verbose "$message" } } # Print to a particular channel a brief summary of taccle command line # options. proc print_taccle_help {chan} { puts $chan "taccle: a Tcl compiler compiler Usage: taccle \[options\] file file a taccle grammar specification file Options: -h print this help message and quit -d write extra output file containing Tcl code to be \[source\]d by yylex -o FILE specify name to write parser -v write extra output file containing descriptions of all parser states and extended information about conflicts -w display all warnings to standard error -p PREFIX change default yy prefix to PREFIX --version print taccle version and quit For more information see http://mini.net/tcl/taccle" } # Displays to standard out the taccle version, then exits program. proc print_taccle_version {} { puts "taccle version $::TACCLE_VERSION" exit 0 } ###################################################################### # internal debugging routines proc print_symbol_table {} { puts $::verbose "token table:" puts $::verbose [format "%-5s %-10s %s" "id" "token" "type"] foreach tok_id $::token_list { set token $::token_id_table($tok_id) if {$::token_id_table($tok_id,t) == $::TERMINAL} { set type "terminal" } else { set type "non-terminal" } puts $::verbose [format "%-5s %-10s %s" $tok_id $token $type] } } proc print_rule_table {} { puts $::verbose "rule table:" for {set i 0} {$i < $::rule_count} {incr i} { set lhs $::token_id_table($::rule_table($i,l)) set deriv_list "" foreach deriv $::rule_table($i,d) { lappend deriv_list $::token_id_table($deriv) } if {$deriv_list == ""} { set deriv_list "\#\# empty \#\#" } puts $::verbose [format "%3d: %-10s -> %s" $i $lhs $deriv_list] } } proc print_first_table {} { puts $::verbose "first table:" foreach tok_id $::token_list { if {$tok_id == -1} { continue } set token $::token_id_table($tok_id) set first_list "" foreach first $::first_table($tok_id) { if {$first >= 0} { lappend first_list $::token_id_table($first) } } puts $::verbose [format "%-10s => %s" $token $first_list] } } proc print_closure {closure_list indent dest} { foreach item $closure_list { foreach {rule lookahead position} $item {} set lhs $::token_id_table($::rule_table($rule,l)) set deriv_list "" set i 0 foreach deriv $::rule_table($rule,d) { if {$i == $position} { lappend deriv_list "." } lappend deriv_list $::token_id_table($deriv) incr i } if {$position == $::rule_table($rule,dc)} { lappend deriv_list "." } set lookahead_list "" foreach la $lookahead { lappend lookahead_list $::token_id_table($la) } puts $dest \ [format "%*s %-10s -> %s, %s" $indent "" $lhs $deriv_list $lookahead_list] } } proc print_lr_table {table_name num_entries} { upvar $table_name table for {set i 0} {$i < $num_entries} {incr i} { puts $::verbose "state $i:" print_closure $table($i) 2 $::verbose if {[info exists table($i,trans)] && [llength $table($i,trans)] >= 1} { puts -nonewline $::verbose [format "%*s transitions:" 2 ""] foreach trans $table($i,trans) { foreach {tok_id nextstate} $trans {} puts -nonewline $::verbose " $::token_id_table($tok_id) => s$nextstate" } puts $::verbose "" } puts $::verbose "" } } proc print_lr1_table {} { puts $::verbose "lr(1) table:" print_lr_table ::lr1_table $::next_lr1_state } proc print_lalr1_table {} { puts $::verbose "lalr(1) table:" print_lr_table ::lalr1_table $::next_lalr1_state } proc print_lalr1_parse {} { puts $::verbose "generated lalr(1) parse table:" puts -nonewline $::verbose "state " foreach tok_id $::used_token_list { set token [string range $::token_id_table($tok_id) 0 4] puts -nonewline $::verbose [format " %-5s" $token] } puts $::verbose "" for {set i 0} {$i < $::next_lalr1_state} {incr i} { puts -nonewline $::verbose [format "%4s " $i] foreach tok_id $::used_token_list { if [info exists ::lalr1_parse($i:$tok_id)] { switch -- $::lalr1_parse($i:$tok_id) { shift { set s "sh" } goto { set s "go" } reduce { set s "re" } accept { set s "accept" } } if {$s != "accept"} { append s $::lalr1_parse($i:$tok_id,target) } puts -nonewline $::verbose [format " %-5s" $s] } else { puts -nonewline $::verbose " " } } puts $::verbose "" } } ###################################################################### # other taccle functions # Parse the taccle command line. proc taccle_args {argv} { set argvp 0 set write_defs_file 0 set write_verbose_file 0 set out_filename "" set ::p "yy" set ::show_warnings 0 while {$argvp < [llength $argv]} { set arg [lindex $argv $argvp] switch -- $arg { "-d" { set write_defs_file 1 } "-h" - "--help" { print_taccle_help stdout; exit 0 } "-o" { set out_filename [get_param $argv argvp "o"] } "-v" - "--verbose" { set write_verbose_file 1 } "-w" { set ::show_warnings 1 } "-p" { set prefix [get_param $argv argvp "p"] set ::p [string tolower $prefix] } "--version" { print_taccle_version } default { if {[string index $arg 0] != "-"} { break } else { puts stderr "taccle: unknown option $arg" print_taccle_help stderr exit $::PARAM_ERROR } } } incr argvp } if {$argvp >= [llength $argv]} { puts stderr "taccle: no grammar file given" print_taccle_help stderr exit $::IO_ERROR } set in_filename [lindex $argv $argvp] if {$out_filename == ""} { set out_filename [file rootname $in_filename] append out_filename ".tcl" } if [catch {open $in_filename r} ::src] { puts stderr "Could not open grammar file '$in_filename'." exit $::IO_ERROR } if [catch {open $out_filename w} ::dest] { puts stderr "Could not open output file '$out_filename'." exit $::IO_ERROR } if $write_defs_file { set header_filename "[file rootname $out_filename].tab.tcl" if [catch {open $header_filename w} ::header] { puts stderr "Could not open header file '$header_filename'." exit $::IO_ERROR } } else { set ::header "" } if $write_verbose_file { set verbose_filename "[file rootname $out_filename].output" if [catch {open $verbose_filename w} ::verbose] { puts stderr "Could not open verbose file '$verbose_filename'." exit $::IO_ERROR } } else { set ::verbose "" } } # Actually do the parser generation. proc taccle_main {} { set ::line_count 0 # counts number of rules in the grammar # rule number 0 is reserved for the special augmentation S' -> S set ::rule_count 1 # used to keep track of token IDs: # 0 is reserved for the special token '$' # 256 for the error token set ::next_token_id 257 # used to keep track of operator precedence level # level 0 is reserved for terminals without any precedence set ::next_precedence 1 # keep track of where within the file I am: # definitions, rules, or subroutines set file_state definitions while {[gets $::src line] >= 0} { incr ::line_count if {$line == "%%"} { if {$file_state == "definitions"} { set file_state "rules" } elseif {$file_state == "rules"} { set file_state "subroutines" } else { taccle_error "Syntax error." $::SYNTAX_ERROR } } else { if {$file_state == "definitions"} { handle_defs $line } elseif {$file_state == "rules"} { # keep reading the rest of the file until EOF or # another '%%' appears set rules_buf [strip_comments $line] while {[gets $::src line] >= 0 && $file_state == "rules"} { if {$line == "%%"} { set file_state "subroutines" } else { append rules_buf "\n" [strip_comments $line] } } build_parser $rules_buf set file_state "subroutines" write_parser_utils write_parser } else { # file_state is subroutines -- copy verbatim to output file puts $::dest $line } } } if {$::header != ""} { write_header_file } if {$::verbose != ""} { print_symbol_table puts $::verbose "" print_rule_table puts $::verbose "" #print_first_table #puts $::verbose "" #print_lr1_table print_lalr1_table print_lalr1_parse } } ###################################################################### # start of actual script set IO_ERROR 1 set SYNTAX_ERROR 2 set PARAM_ERROR 3 set GRAMMAR_ERROR 4 set TERMINAL 0 set NONTERMINAL 1 taccle_args $argv taccle_main