############################################################################ # # File: ipp.icn # # Subject: Program to preprocess Icon programs # # Author: Robert C. Wieland, revised by Frank J. Lhota # # Date: March 26, 2002 # ############################################################################ # # This file is in the public domain. # ############################################################################ # # Ipp is a preprocessor for the Icon language. Ipp has many operations and # features that are unique to the Icon environment and should not be used as # a generic preprocessor (such as m4). Ipp produces output which when written # to a file is designed to be the source for icont, the command processor for # Icon programs. # ############################################################################ # # Ipp may be invoked from the command line as: # # ipp [option ...] [ifile [ofile]] # # Two file names may be specified as arguments. 'ifile' and 'ofile' are # respectively the input and output files for the preprocessor. By default # these are standard input and standard output. If the output file is to be # specified while the input file should remain standard input a dash ('-') # should be given as 'ifile'. For example, 'ipp - test' makes test the output # file while retaining standard input as the input file. # # The following special names are predefined by ipp and may not be # redefined # or undefined. The name _LINE_ is defined as the line number # (as an integer) of the line of the source file currently processed. The # name _FILE_ is defined as the name of the current source file # (as a string). If the source is standard input then it has the value # 'stdin'. # # Ipp will also set _LINE_ and _FILE_ from the "#line" directives it # encounters, and will insert line directives to indicate source origins. # # Also predefined are names corresponding to the features supported by the # implementation of Icon at the location the preprocessor is run. This allows # conditional translations using the 'if' commands, depending on what features # are available. Given below is a list of the features on a 4.nbsd UNIX # implementation and the corresponding predefined names: # # Feature Name # ----------------------------------------------------- # UNIX UNIX # co-expressions co_expressions # overflow checking overflow_checking # direct execution direct_execution # environment variables environment_variables # error traceback error_traceback # executable images executable_images # string invocation string_invocation # expandable regions expandable_regions # # # Command-Line Options: # --------------------- # # The following options to ipp are recognized: # # -C By default ipp strips Icon-style comments. If this option # is specified all comments are passed along except those # found on ipp command lines (lines starting with a '$' # command). # # -D name # -D name=def Allows the user to define a name on the command line instead # of using a $define command in a source file. In the first # form the name is defined as '1'. In the second form name is # defined as the text following the equal sign. This is less # powerful than the $define command line since def can not # contain any white space (spaces or tabs). # # -d depth By default ipp allows include files to be nested to a depth # of ten. This allows the preprocessor to detect infinitely # recursive include sequences. If a different limit for the # nesting depth is needed it may changed by using this option # with an integer argument greater than zero. Also, if a file # is found to already be in a nested include sequence an # error message is written regardless of the limit. # # -I dir The following algorithm is normally used in searching for # $include files. On a UNIX system names enclosed in "" are # searched for by trying in order the directories specified by the # PATH environment variable, and names enclosed in <> are always # expected to be in the /usr/icon/src directory. On other systems # names enclosed in <> are searched for by trying in order the # directories specified by the IPATH environment variable; names # in "" are searched for in a similar fashion, except that the # current directory is tried first. If the -I option is given the # directory specified is searched before the 'standard' # directories. If this option is specified more than once the # directories specified are tried in the order that they appear on # the command line, then followed by the 'standard' directories. # # Preprocessor commands: # ---------------------- # # All ipp commands start with a line that has '$' as its first non-space # character. The name of the command must follow the '$'. White space # (any number of spaces or tabs) may be used to separate the '$' and the # command name. Any line beginning with a '$' and not followed by a valid # name will cause an error message to be sent to standard error and # termination of the preprocessor. If the command requires an argument then # it must be separated from the command name by white space otherwise the # argument will be considered part of the name and the result will likely # produce an error. In processing the $ commands ipp responds to exceptional # conditions in one of two ways. It may produce a warning and continue # processing or produce an error message and terminate. In both cases the # message is sent to standard error. With the exception of error conditions # encountered during the processing of the command line, the messages normally # include the name and line number of the source file at the point the # condition was encountered. Ipp was designed so that most exception # conditions encountered will produce errors and terminate. This protects the # user since warnings could simply be overlooked or misinterpreted. # # Many ipp command require names as arguments. Names must begin with a # letter or an underscore, which may be followed by any number of letters, # underscores, and digits. Icon-style comments may appear on ipp command # lines, however they must be separated from the normal end of the command by # white_space. If any extraneous characters appear on a command line a # warning is issued. This occurs when characters other than white-space or a # comment follow the normal end of a command. # # The following commands are implemented: # # $define: This command may be used in one of two forms. The first form # only allows simple textual substitution. It would be invoked as # '$define name text'. Subsequent occurrences of name are replaced # with text. Name and text must be separated by one white space # character which is not considered to be part of the replacement # text. Normally the replacement text ends at the end of the line. # The text however may be continued on the next line if the backslash # character '\' is the last character on the line. If name occurs # in the replacement text an error message (recursive textual substi- # tution) is written. # # The second form is '$define name(arg,...,arg) text' which defines # a macro with arguments. There may be no white space between the # name and the '('. Each occurrence of arg in the replacement text # is replaced by the formal arg specified when the macro is # encountered. When a macro with arguments is expanded the arguments # are placed into the expanded replacement text unchanged. After the # entire replacement text is expanded, ipp restarts its scan for names # to expand at the beginning of the newly formed replacement text. # As with the first form above, the replacement text may be continued # on following lines. The replacement text starts immediately after # the ')'. # The names of arguments must comply with the convention for regular # names. See the section below on Macro processing for more # information on the replacement process. # # $undef: Invoked as '$undef name'. Removes the definition of name. If # name is not a valid name or if name is one of the reserved names # _FILE_ or _LINE_ a message is issued. # # $include: Invoked as '$include ' or '$include "filename"'. This # causes the preprocessor to make filename the new source until # end of file is reached upon which input is again taken from the # original source. See the -I option above for more detail. # # $dump: This command, which has no arguments, causes the preprocessor to # write to standard error all names which are currently defined. # See '$ifdef' below for a definition of 'defined'. # # $warning: # This command issues a warning, with the text coming from the # argument field of the command. # # $error: This command issues a error, with the text coming from the # argument field of the command. As with all errors, processing # is terminated. # # $ifdef: Invoked as 'ifdef name'. The lines following this command appear # in the output only if the name given is defined. 'Defined' means # 1. The name is a predefined name and was not undefined using # $undef, or # 2. The name was defined using $define and has not been undefined # by an intervening $undef. # # $ifndef: Invoked as 'ifndef name'. The lines following this command do # not appear in the output if the name is not defined. # # $if: Invoked as 'if constant-expression'. Lines following this # command are processed only if the constant-expression produces a # result. The following arithmetic operators may be applied to # integer arguments: + - * / % ^ # # If an argument to one of the above operators is not an integer an # error is produced. # # The following functions are provided: def(name), ndef(name) # This allows the utility of $ifdef and $ifndef in a $if command. # def produces a result if name is defined and ndef produces a # result if name is not defined. # # The following comparison operators may be used on integer # operands: # # > >= = < <= ~= # # Also provided are alternation (|), conjunction (&), and # negation (not). The following table lists all operators with # regard to decreasing precedence: # # not + - (unary) # ^ (associates right to left) # * / % # + - (binary) # > >= = < <= ~= # | # & # # The precedence of '|' and '&' are the same as the corresponding # Icon counterparts. Parentheses may be used for grouping. # Backtracking is performed, so that the expression # # FOO = (1|2) # # will produce a result precisely when FOO is either 1 or 2. # # $elif: Invoked as 'elif constant-expression'. If the lines preceding # this command were processed, this command and the lines following # it up to the matching $endif command are ignored. Otherwise, # the constant-expression is evaluated, and the lines following this # command are processed only if it produces a result. # # $else: This command has no arguments and reverses the notion of the # test command which matches this directive. If the lines preceding # this command where ignored the lines following are processed, and # vice versa. # # $endif: This command has no arguments and ends the section of lines # begun by a test command ($ifdef, $ifndef, or $if). Each test # command must have a matching $endif. # # Macro Processing and Textual Substitution # ----------------------------------------- # No substitution is performed on text inside single quotes (cset literals) # and double quotes (strings) when a line is processed. The preprocessor # will # detect unclosed cset literals or strings on a line and issue an # error message unless the underscore character is the last character on the # line. The output from # # $define foo bar # write("foo") # # is # # write("foo") # # Unless the -C option is specified comments are stripped from the source. # Even if the option is given the text after the '#' is never expanded. # # Macro formal parameters are recognized in $define bodies even inside cset # constants and strings. The output from # # $define test(a) "a" # test(processed) # # is the following sequence of characters: "processed". # # Macros are not expanded while processing a $define or $undef. Thus: # # $define off invalid # $define bar off # $undef off # bar # # produces off. The name argument to $ifdef or $ifndef is also not expanded. # # Mismatches between the number of formal and actual parameters in a macro # call are caught by ipp. If the number of actual parameters is greater than # the number of formal parameters is error is produced. If the number of # actual parameters is less than the number of formal parameters a warning is # issued and the missing actual parameters are turned into null strings. # ############################################################################ # # The records and global variables used by ipp are described below: # # Src_desc: Record which holds the 'file descriptor' and name # of the corresponding file. Used in a stack to keep # track of the source files when $includes are used. # Opt_rec Record returned by the get_args() routine which returns # the options and arguments on the command line. options # is a cset containing options that have no arguments. # pairs is a list of [option, argument] pairs. ifile and # ofile are set if the input or output files have been # specified. # Defs_rec Record stored in a table keyed by names. Holds the # names of formal arguments, if any, and the replacement # text for that name. # Expr_node Node of a parse tree for $if / $elif expressions. # Holds the operator, or a string representing the # control structure. Also, holds a list of the args for # the operation / control structure, which are either # scalars or other Expr_node records. # Chars Cset of all characters that may appear in the input. # Defs The table holding the definition data for each name. # Depth The maximum depth of the input source stack. # Ifile Descriptor for the input file. # Ifile_name Name of the input file. # Init_name_char Cset of valid initial characters for names. # Line_no The current line number. # Name_char Cset of valid characters for names. # Non_name_char The complement of the above cset. # Ofile The descriptor of the output file. # Options Cset of no-argument options specified on the command # line. # Path_list List of directories to search in for "" include files. # Src_stack The stack of input source records. # Std_include_paths List of directories to search in for <> include files. # White_space Cset for white-space characters. # TRUE Defined as 1. # ############################################################################ record Src_desc(fd, fname, line) record Opt_rec(options, pairs, ifile, ofile) record Defs_rec(arg_list, text) record Expr_node(op, arg) global Chars, Defs, Depth, Ifile, Ifile_name, Init_name_char, Line_no, Name_char, Non_name_char, Ofile, Options, Path_list, Src_stack, Std_include_paths, White_space, TRUE, DIR_SEP procedure main(arg_list) local line, source init(arg_list) repeat { while line := get_line(Ifile) do line ? process_cmd(get_cmd()) # Get new source close(Ifile) if source := pop(Src_stack) then { Ifile := source.fd Ifile_name := source.fname Line_no := source.line } else break } end procedure conditional(expr) return if eval(expr) then true_cond() else false_cond() end # # In order to simplify the parsing the four operators that are longer # than one character (<= ~= >= not) are replaced by one character # 'aliases'. Also, all white space is removed. # procedure const_expr(expr) local new static White_space_plus initial White_space_plus := White_space ++ '<>~n' new := "" expr ? { while new ||:= tab(upto(White_space_plus)) || if any(White_space) then { tab(many(White_space)) "" } else if =">=" then "\x01" else if ="<=" then "\x02" else if ="~=" then "\x03" else if not any(Name_char, ,&pos - 1) & ="not" & not any(Name_char) then "\x04" else move (1) new ||:= tab(0) } # # Now recursively parse the transformed string. # return parse(new) end procedure decoded(op) return case op of { "\x01": ">=" "\x02": "<=" "\x03": "~=" "\x04": "not" default: op } end procedure def_opt(s) local name, text s ? { name := tab(find("=")) | tab(0) text := (move(1) & tab(0)) | "1" } if name == ("_LINE_" | "_FILE_") then error(name, " is a reserved name and can not be redefined by the -D option") if not name ? (get_name() & pos(0)) then error(name, " : Illegal name argument to -D option") if member(Defs, name) then warning(name, " : redefined by -D option") insert(Defs, name, Defs_rec(, text)) end procedure define() local args, name, text get_opt_ws() if name := get_name() & (any(White_space ++ '(') | pos(0)) then { if name == ("_LINE_" | "_FILE_") then error(name, " is a reserved name and can not be redefined") if match("(") then # A macro args := get_formals() text := get_text(args) if member(Defs,name) then warning(name, " redefined") insert(Defs, name, Defs_rec(args, text)) } else error("Illegal or missing name in define") end procedure dump() if not pos(0) then warning("Extraneous characters after dump command") every write(&errout, (!sort(Defs))[1]) end procedure error(s1, s2) s1 ||:= \s2 stop(Ifile_name, ": ", Line_no, ": ", "Error ", s1) end procedure eval(node) suspend case type(node) of { "Expr_node": { case node.op of { "|" : eval(node.arg[1]) | eval(node.arg[2]) "&" : eval(node.arg[1]) & eval(node.arg[2]) "not" : not eval(node.arg[1]) "def" : member(Defs, node.arg[1]) "ndef" : not member(Defs, node.arg[1]) default : case *node.arg of { 1 : node.op(eval(node.arg[1])) 2 : node.op(eval(node.arg[1]), eval(node.arg[2])) } } } default: node } end procedure false_cond() local cmd, line # Skip to next $else / $elif branch, or $endif cmd := skip_to("elif", "else", "endif") case cmd of { "elif" : return if_cond(cmd) "else" : { while line := get_line(Ifile) do line ? { cmd := get_cmd() case cmd of { "elif" : error("'elif' encountered after 'else'") "else" : error("multiple 'else' sections") "endif" : return default : process_cmd(cmd) } } error("'endif' not encountered before end of file") } "endif": return } end procedure find_file(fname, path_list) local ifile, ifname, path every path := !path_list do { ifname := if path == ("" | ".") then fname else path || DIR_SEP || fname if ifile := open(ifname) then { if *Src_stack >= Depth then { close(ifile) error("Possibly infinitely recursive file inclusion") } if ifname == (Ifile_name | (!Src_stack).fname) then error("Infinitely recursive file inclusion") push(Src_stack, Src_desc(Ifile, Ifile_name, Line_no)) Ifile := ifile Ifile_name := ifname Line_no := 0 return } } error("Can not open include file ", fname) end procedure func(expr) local op, arg expr ? { if op := tab(find("(")) & move(1) & arg := get_name() & =")" & pos(0) then { if op == ("def" | "ndef") then return Expr_node(op, [arg]) else error("Invalid function name") } } end procedure get_args(arg_list, simple_opts, arg_opts) local arg, ch, get_ofile, i, opts, queue opts := Opt_rec('', []) queue := [] every arg := arg_list[i := 1 to *arg_list] do if arg == "-" then # Next argument should be output file get_ofile := (i = *arg_list - 1) | stop("Invalid position of '-' argument") else if arg[1] == "-" then # Get options every ch := !arg[2: 0] do if any(simple_opts, ch) then opts.options ++:= ch else if any(arg_opts, ch) then put(queue, ch) else stop("Invalid option - ", ch) else if ch := pop(queue) then # Get argument for option push(opts.pairs, [ch, arg]) else if \get_ofile then { # Get output file opts.ofile := arg get_ofile := &null } else { # Get input file opts.ifile := arg get_ofile := (i < *arg_list) } if \get_ofile | *queue ~= 0 then stop("Invalid number of arguments") return opts end procedure get_cmd() local cmd static no_arg_cmds initial no_arg_cmds := set(["dump", "else", "endif"]) if ="#" & cmd := ="line" then get_opt_ws() else if (get_opt_ws()) & ="$" then { get_opt_ws() (cmd := tab(many(Chars))) | error("Missing command") get_opt_ws() if not pos(0) & member(no_arg_cmds, cmd) then warning("Extraneous characters after argument to '" || cmd || "'") } else tab (1) return cmd end procedure get_formals() local formal, arglist, ch arglist := [] ="(" get_opt_ws() if not =")" then repeat { if (formal := get_name()) & get_opt_ws() & any(',)') then put(arglist, formal) else error("Invalid formal argument in macro definition") if =")" then break ="," get_opt_ws() } get_opt_ws() return arglist end procedure get_line(Ifile) return 1(read(Ifile), Line_no +:= 1) end procedure get_name() return tab(any(Init_name_char)) || (tab(many(Name_char)) | "") end procedure get_opt_ws() return (tab(many(White_space)) | "") || (="#" || tab(0) | "") end procedure get_text(is_macro) local text if \is_macro then text := tab(0) else text := (tab(any(White_space)) & tab(0)) | "" while (text[-1] == "\\") do (text := text[1:-1] || get_line(Ifile)) | error("Continuation line not found before end of file") return text end # if_cond is the procedure for $if or $elif. # # Procedure true_cond is invoked if the evaluation of a previous $if, $ifdef, or # $ifndef causes subsequent lines to be processed. Lines will be processed # upto an $elif, $else, or $endif. If $elif or $else is encountered, lines # are skipped until the matching $endif is encountered. # # Procedure false_cond is invoked if the evaluation of a previous $if, $ifdef, # or $ifndef causes subsequent lines to be skipped. Lines will be skipped # upto an $elif, $else, or, $endif. If $else is encountered, lines are # processed until the $endif matching the $else is encountered. procedure if_cond(cmd) if pos(0) then error("Constant expression argument to '" || cmd || "' missing") else return conditional(const_expr(tab(0))) end procedure ifdef() local name if name := get_name() then { get_opt_ws() if not pos(0) then warning("Extraneous characters after argument to 'ifdef'") return conditional(Expr_node("def", [name])) } else error("Argument to 'ifdef' is not a valid name") end procedure ifndef() local name if name := get_name() then { get_opt_ws() if not pos(0) then warning("Extraneous characters after argument to 'ifndef'") return conditional(Expr_node("ndef", [name])) } else error("Argument to 'ifndef' is not a valid name") end procedure in_text(name, text) return text ? tab(find(name)) & (if move(-1) then tab(any(Non_name_char)) else "") & move(*name) & (tab(any(Non_name_char)) | pos(0)) end procedure include() local ch, fname static fname_chars, stopper initial { fname_chars := Chars -- '<>"' stopper := table() insert(stopper, "\"", "\"") insert(stopper, "<", ">") } if (ch := tab(any('"<'))) & (fname := tab(many(fname_chars))) & =stopper[ch] then { get_opt_ws() if not pos(0) then warning("Extraneous characters after include file name") find_file(fname, case ch of { "\"" : Path_list "<" : Std_include_paths } ) } else error("Missing or invalid include file name") end procedure init(arg_list) local s TRUE := 1 Defs := table() Init_name_char := &letters ++ '_' Name_char := Init_name_char ++ &digits Non_name_char := ~Name_char White_space := ' \t\b' Chars := &ascii -- White_space Line_no := 0 Depth := 10 # Predefine features every s := &features do { s := map(s, " -/", "___") insert(Defs, s, Defs_rec(, "1")) } # Set path list for $include files given in "", <> if member(Defs, "UNIX") then { Path_list := [] getenv("PATH") ? while put(Path_list, 1(tab(find(":")), move(1))) Std_include_paths := ["/usr/icon/src"] } else { Std_include_paths := [] (getenv("IPATH") || " ") ? while put(Std_include_paths, tab(find(" "))) do move(1) Path_list := [""] ||| Std_include_paths } process_options(arg_list) end procedure lassoc(expr, op) local j, arg1, arg2 expr ? { every j := bal(op) # Succeeds if op found. if arg1 := tab(\j) & op := decoded(move(1)) & arg2 := tab(0) then { op := proc(op, 2) # Fails for control structures return Expr_node(op, [parse(arg1), parse(arg2)]) } } end # # Programmer's note: Ifile_name and Line_no should not be assigned new # values until the very end, so that if there is an error, the error # message will include the file/line no of the current line directive, # instead of the file/line of the text that follows the directive. # procedure line() local new_line, new_file new_line := tab(many(&digits)) | error("No line number in line directive") get_opt_ws() if ="\"" then { new_file := "" # # Get escaped chars. We assume that the only escaped chars # appearing in a file name would be \\ or \", where the actual # character to be used is simply the character following the slash. # In the unlikely event that other escape sequences are encountered, # this section would have to revised. # while new_file ||:= tab(find("\\")) || (move(1) & move(1)) new_file ||:= tab(find("\"")) | error("Invalid file name in line directive") } Line_no := integer(new_line) Ifile_name := \new_file return end procedure macro_call(entry, args) local i, value, result, token value := table() every i := 1 to *entry.arg_list do insert(value, entry.arg_list[i], args[i] | "") entry.text ? { result := tab(upto(Name_char) | 0) while token := tab(many(Name_char)) do { result ||:= \value[token] | token result ||:= tab(many(Non_name_char)) } } return result end procedure no_endif_error() error("'endif' not encountered before end of file") end procedure parse(expr) # strip surrounding parens. while expr ?:= 2(="(", tab(bal (')')), pos(-1)) return lassoc(expr, '&' | '|') | lassoc(expr, '<=>\x01\x02\x03' | '+-' | '*/%') | rassoc(expr, '^') | unary(expr, '+-\x04') | func(expr) | integer(process_text(expr)) | error(expr, " : Integer expected") end procedure process_cmd(cmd) static last_cmd initial last_cmd := "" case cmd of { "dump" : dump() "define" : define() "undef" : undefine() "include" : include() "line" : line() "error" : error(tab(0)) "warning" : warning(tab(0)) "if" : if_cond( last_cmd := cmd ) "ifdef" : ifdef( last_cmd := cmd ) "ifndef" : ifndef( last_cmd := cmd ) "elif" | "else" | "endif" : error("No previous 'if' expression") &null : { if \last_cmd then put_linedir(Ofile, Line_no, Ifile_name) write(Ofile, process_text(tab(0))) } default : error("Undefined command") } last_cmd := cmd return end procedure process_macro(name, entry, s) local arg, args, new_entry, news, token s ? { args := [] if ="(" then { # # Get args if list is not empty. # get_opt_ws () if not =")" then repeat { arg := get_opt_ws() if token := tab(many(Chars -- '(,)')) then { if /(new_entry := Defs[token]) then arg ||:= token else if /new_entry.arg_list then arg ||:= new_entry.text else { # Macro with arguments if news := tab(bal(White_space ++ ',)')) then arg ||:= process_macro(token, new_entry, news) else error(token, ": Error in arguments to macro call") } # if } # if else if not any(',)') then error(name, ": Incomplete macro call") arg ||:= tab(many(White_space)) put(args, arg) if match(")") then break move(1) } # repeat if *args > *entry.arg_list then error(name, ": Too many arguments in macro call") else if *args < *entry.arg_list then warning(name, ": Missing arguments in macro call") return macro_call(entry, args) } # if } end procedure process_options(arg_list) local args, arg_opts, pair, simple_opts, tmp_list, value simple_opts := 'C' arg_opts := 'dDI' Src_stack := [] args := get_args(arg_list, simple_opts, arg_opts) if \args.ifile then { (Ifile := open(args.ifile)) | stop("Can not open input file ", args.ifile) Ifile_name := args.ifile } else { Ifile := &input Ifile_name := "stdin" } if \args.ofile then (Ofile := open(args.ofile, "w")) | stop("Can not open output file", args.ofile) else Ofile := &output Options := args.options tmp_list := [] every pair := !args.pairs do case pair[1] of { "D": def_opt(pair[2]) "d": if (value := integer(pair[2])) > 0 then Depth := value else stop("Invalid argument for depth") "I": push(tmp_list, pair[2]) } Path_list := tmp_list ||| Path_list end procedure process_text(line) local add, entry, new, position, s, token static in_string, in_cset new := "" while *line > 0 do { add := "" line ?:= { if \in_string then { # Ignore escaped chars while new ||:= tab(find("\\")) || move(2) if new ||:= tab(find("\"")) || move(1) then in_string := &null else { new ||:= tab(0) if line[-1] ~== "_" then { in_string := &null warning("Unclosed double quote") } } } else if \in_cset then { # Ignore escaped chars. while new ||:= tab(find("\\")) || move(2) if new ||:= (tab(find("'")) || move(1)) then in_cset := &null else { new ||:= tab(0) if line[-1] ~== "_" then { in_cset := &null warning("Unclosed single quote") } } } new ||:= tab(many(White_space)) case token := tab(many(Name_char) | any(Non_name_char)) of { "\"": { new ||:= "\"" if \in_string then in_string := &null else if not pos(0) then { in_string := TRUE } else { warning("Unclosed double quote") } add ||:= tab(0) } "'": { new ||:= "'" if \in_cset then in_cset := &null else if not pos(0) then { in_cset := TRUE } else { warning("Unclosed double quote") } add ||:= tab(0) } "#": { new ||:= if any(Options, 'C') then token || tab(0) else tab(0) & token ? tab(find("#")) } "__LINE__": new ||:= Line_no "__FILE__": new ||:= Ifile_name default: { if /(entry := Defs[token]) then new ||:= token else if /entry.arg_list then if in_text(token, entry.text) then error("Recursive textual substitution") else add := entry.text else { # Macro with arguments s := tab(bal(White_space) | 0) if not any('(', s) then error(token, ": Incomplete macro call") add := process_macro(token, entry, s) } } # default } # case add || tab(0) } # ?:= } # while return new end procedure put_linedir(Ofile, Line_no, Ifile_name) static last_filename initial last_filename := "" writes(Ofile, "#line ", Line_no - 1) # # Output file name part only if the # filename differs from the last one used. # if last_filename ~==:= Ifile_name then writes(Ofile, " ", image(last_filename)) write(Ofile) return end procedure rassoc(expr, op) local arg1, arg2 # Succeeds if op found. expr ? if arg1 := tab(bal(op)) & op := move(1) & arg2 := tab(0) then { op := decoded(op) op := proc(op, 2) # Fails for control structures return Expr_node(op, [parse(arg1), parse(arg2)]) } end # # skip_to is used to skip over parts of the an '$if' structure. targets # are the $if - related commands to skip to, and should always include # "endif". # # We do not, of course, wish to skip to a command in an $if structure # that is embedded in the current one; also, we want to make sure that # embedded $if structures, even in skipped lines, are well formed. We # therefore maintain a stack, if_sects, of the currently applicable $if # structure commands encountered in the skipped lines. For example, if # we have skipped over the commands # # $ifdef ... # $if ... # $elif ... # $if ... # $else # # if_sect would be ["else", "elif", "ifdef"]. # procedure skip_to(targets[]) local cmd, if_sects, line, argpos if_sects := [] while line := get_line(Ifile) | no_endif_error () do line ? { cmd := get_cmd() if *if_sects = 0 & \cmd == !targets then { argpos := &pos break } case cmd of { "if" | "ifdef" | "ifndef" : { if pos(0) then error("Argument to '" || cmd || "' missing") push(if_sects, cmd) } "elif" : { if pos(0) then error("Argument to '" || cmd || "' missing") if if_sects[1] == "else" then error("'elif' encountered after 'else'") else if_sects[1] := cmd } "else" : { if if_sects[1] == "else" then error("multiple 'else' sections") else if_sects[1] := cmd } "endif" : pop(if_sects) } } # # Now reset the &subject to the current line, and &pos to the argument # field of the current line, so that if we skipped to a line which will # require further processing (such as $elif), the scanning functions can # be used. # &subject := line &pos := argpos return cmd end procedure true_cond() local cmd, line while line := get_line(Ifile) | no_endif_error () do line ? { case cmd := get_cmd() of { "elif" | "else" : return skip_to("endif") "endif" : return cmd default : process_cmd(cmd) } } end procedure unary(expr, op) local arg1 # Succeeds if op found. expr ? if op := decoded(tab(any(op))) & arg1 := tab(0) then { op := proc(op, 1) # fails for control structures return Expr_node(op, [parse(arg1)]) } end procedure undefine() local name if name := get_name() then { get_opt_ws() if not pos(0) then warning("Extraneous characters after argument to undef") if name == ("_LINE_" | "_FILE_") then error(name, " is a reserved name that can not be undefined") delete(Defs, name) } else error("Name missing in undefine") end procedure warning(s1, s2) s1 ||:= \s2 write(&errout, Ifile_name, ": ", Line_no, ": ", "Warning " || s1) end