############################################################################ # # File: itweak.icn # # Subject: Icon interactive debugging. # Tweaks a ucode file ('.u1') to invoke a debugging procedure. # # Author: Hakan Soderstrom # # Revision: $Revision: 2.21 $ # ########################################################################### # # Copyright (c) 1994 Hakan Soderstrom and # Soderstrom Programvaruverkstad AB, Sweden # # Permission to use, copy, modify, distribute, and sell this software # and its documentation for any purpose is hereby granted without fee, # provided that the above copyright notice and this permission notice # appear in all copies of the software and related documentation. # # THE SOFTWARE IS PROVIDED "AS-IS" AND WITHOUT WARRANTY OF ANY KIND, # EXPRESS, IMPLIED OR OTHERWISE, INCLUDING WITHOUT LIMITATION, ANY # WARRANTY OF MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. # # IN NO EVENT SHALL HAKAN SODERSTROM OR SODERSTROM PROGRAMVARUVERKSTAD # AB BE LIABLE FOR ANY SPECIAL, INCIDENTAL, INDIRECT OR CONSEQUENTIAL # DAMAGES OF ANY KIND, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS # OF USE, DATA OR PROFITS, WHETHER OR NOT ADVISED OF THE POSSIBILITY # OF DAMAGE, AND ON ANY THEORY OF LIABILITY, ARISING OUT OF OR IN # CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. # ########################################################################### # #-------- Record types -------- # record l_decl (d_type, d_serial, d_code, d_name, d_displ, ld_cserial, ld_dbg) # Holds a 'local' declaration. # 'd_type' must be the declaration type (integer), in this case, $define D_LOCAL 1 # 'd_serial' must be the serial number of the declaration (integer). # 'd_code' must be the bitfield that further characterizes the declaration. # It is stored as the integer obtained by interpreting the octal coded # bitfield as a decimal number. # 'd_name' must be the source name of the declared entity. # 'd_displ' must be non-null to indicate that this declaration is to be # passed to the debug procedure. # 'ld_cserial' may be a constant serial number (integer), or null. # If integer then the name of this local exists as a constant in the current # procedure, which means we include it among the visible variables. # 'ld_dbg' is non-null if the declaration has been added by this program. record c_decl (d_type, d_serial, d_code, d_name, d_displ) # Holds a constant declaration added by the program. # Like 'l_decl', except 'd_type' must be $define D_CONST 2 record fmap (fm_ucode, fm_source) # Holds the mapping between an ucode file name and a source file name. # 'fm_ucode' must be the root of an ucode file name (string). # I.e. the file name without the trailing '.u?'. # 'fm_source' must be the name of the source file from which the ucode # file originates (string). global file_map # Set containing mapping between ucode and source files (set of record fmap). global file_root, uin, uout, ulno # The current root file name (i.e. file name without '.u?'). # The current ucode input file. # The current ucode output file. # The current line number in the current ucode input file. global init_file # Output file name: init file. global msgout # Message output file. global proc_hil # Table containing the "high label" of each procedure in a ucode file. # Entry key is a procedure name (string). # Entry value is the numeric part of the highest existing label before # debugification (integer). global white # This program's definition of white space. # #-------- Constants -------- # # Version of this program, variable for holding it. $define PROGRAM_VERSION "$Revision: 2.21 $" $define PROG_VERSION_VAR "__dbg_itweak_ver" # DEBUGGING IDENTIFIERS. # List holding breakpoints for one source file; two parts. # The root file name should be spliced in between. $define DBG_BRKP1 "__dbg_file_" $define DBG_BRKP2 "_brkp" # Global variable holding source/ucode file map. # Note: any change affects 'dbg.icn' as well. $define DBG_FILE_MAP "__dbg_file_map" # Procedure for initializing debugging globals. $define DBG_INIT "__dbg_init" # Local variable: trapped line number. $define DBG_LINE "__dbg_line" # List containing names of interesting local variables. $define DBG_NAME "__dbg_name" # Procedure to call on break. $define DBG_PROC "__dbg_proc" # Procedure deciding on break. $define DBG_TEST "__dbg_test" # Name of variable whose presence is taken as assurance that an ucode # file has been tweaked. $define DBG_SENTINEL DBG_LINE # Default file name for writing the debug initialization code. $define DBG_INIT_FILE "dbg_init.icn" # File name for the debugging run-time. $define DBG_RUN_TIME "dbg_run.u1" # Ucode 'codes' (bitfields) for local declarations. # The values are the octal coded bitfield interpreted as decimal. $define LD_GLOBAL 0 $define LD_LOCAL 20 $define LD_PARM 1000 $define LD_STATIC 40 # Ucode 'codes' (bitfields) for constant declarations. $define CD_INT 2000 $define CD_STRING 10000 # Various ucode op-codes. $define OP_CONST "con" $define OP_DEND "declend" $define OP_END "end" $define OP_FILEN "filen" $define OP_LABEL "lab" $define OP_LINE "line" $define OP_LOCAL "local" $define OP_PROC "proc" # Op-codes in the '.u2' file. $define OP_VERSION "version" $define OP_LINK "link" $define OP_GLOBAL "global" # Icon versions for which the program has been tested. $define ICON_VER_LO "U8.10.00" $define ICON_VER_HI "U9.0.00" # Prefix used for labels. $define ULAB_PREF "L" $define NALN -1 # Not A Line Number. $define PROGNAME "itweak" # The name by which the user knows this program. $define U1 ".u1" $define U2 ".u2" # Standard ucode file name suffix. $define U1TMP ".uA" $define U2TMP ".uB" # Suffix of temporary ucode file. $define U1OLD ".u1~" $define U2OLD ".u2~" # Suffix of renamed, original ucode file. # #-------- Main -------- # procedure main (argv) local file_names, iout, u2count # Initialize globals. file_map := set () msgout := &errout white := '\t ' # Process command line options; leave a list of file names. if argv[1] == "-o" then { get (argv) (init_file := get (argv)) | confl ("'-o' requires a file name") } else init_file := DBG_INIT_FILE file_names := copy (argv) # The number of tweaked '.u2' files. u2count := 0 # Do two passes on each file. every file_root := !file_names do { # Allow for 'file.u1' and 'file.u'. file_root := if file_root[-3:0] == ".u1" then file_root[1:-3] else if file_root[-2:0] == ".u" then file_root[1:-2] # Pass 1. (uin := open (file_root || U1, "r")) | confl ("Cannot open '%1%2' for input.", file_root, U1) uout := &null if pass1 () then { close (uin) # Tweak at most one '.u2' file. if u2count = 0 then { (uin := open (file_root || U2, "r")) | confl ("Cannot open '%1%2' for input.", file_root, U2) (uout := open (file_root || U2TMP, "w")) | confl ("Cannot open '%1%2' for output.", file_root, U2TMP) u2tweak () close (uin) close (uout) u2count +:= 1 # Make way for the following rename. remove (file_root || U2OLD) rename (file_root || U2, file_root || U2OLD) | confl ("Cannot rename '%1%2' to '%1%3'.", file_root, U2, U2OLD) rename (file_root || U2TMP, file_root || U2) | confl ("Cannot rename '%1%2' to '%1%3'.", file_root, U2TMP, U2) } # Pass 2. (uin := open (file_root || U1, "r")) | confl ("Cannot open '%1%2' for input.", file_root, U1) (uout := open (file_root || U1TMP, "w")) | confl ("Cannot open '%1%2' for output.", file_root, U1TMP) pass2 () close (uin) close (uout) # Make way for the following rename. remove (file_root || U1OLD) rename (file_root || U1, file_root || U1OLD) | confl ("Cannot rename '%1%2' to '%1%3'.", file_root, U1, U1OLD) rename (file_root || U1TMP, file_root || U1) | confl ("Cannot rename '%1%2' to '%1%3'.", file_root, U1TMP, U1) } else { close (uin) note ("'%1%2' seems to be tweaked already; left untouched.", file_root, U1) } } # Write initialization code. (iout := open (init_file, "w")) | confl ("Cannot open '%1' for output.", init_file) cre_init (iout) note ("Initialization code written to '%1'.", init_file) end # #-------- Pass 1 procedures -------- # procedure pass1 () # Performs a first pass over a ucode file, collecting label statistics. # RETURNS null normally. # FAILS if the first procedure has a local declaration containing the sentinel # variable. # This is taken to imply that the ucode file is already tweaked. # SIDE EFFECT: Updates glocal 'proc_hil' (max labels per proc). # Updates 'file_map' (source file name ~ ucode file name). local cur_high, cur_proc, labint, line, loc, op, proc_no static fn_instr, lc_decl initial { fn_instr := [OP_FILEN, OP_LINE, OP_LABEL] lc_decl := [OP_LOCAL, OP_CONST, OP_DEND] } proc_hil := table () loc := table () proc_no := 0 while op := p1_proclab () do if op[1] == "proc" then { if \cur_proc then { (/proc_hil[cur_proc] := cur_high) | confl ("%1: occurs twice; confusing.", cur_proc) } cur_proc := op[2] cur_high := -1 # Special treatment of the first procedure in every file. if (proc_no +:= 1) = 1 then { # Borrow some pass 2 code to collect the local declarations. while (op := p2_upto (lc_decl))[1] == OP_LOCAL do p2_getlocal (loc, op[2]) # Look for source file name. repeat if (op := p2_upto (fn_instr))[1] == OP_FILEN then { insert (file_map, fmap (file_root, op[2])) break } else if op[1] == OP_LABEL then cur_high <:= integer (op[2][2:0]) # Flush buffers. p2_upto () # Fail if the sentinel is present. if \loc[DBG_SENTINEL] then fail } } else if op[1] == "lab" then { # ASSUME the label consists of one character followed by an integer. (labint := integer (op[2][2:0])) | intern ("pass1: Problem parsing label %1.", image (op[2])) cur_high <:= labint } if \cur_proc then { (/proc_hil[cur_proc] := cur_high) | confl ("%1: occurs twice; confusing.", cur_proc) } else intern ("pass1: No proc found.") return &null end procedure p1_proclab () # Returns the next ucode line containing a "proc" or "lab" instruction. # If a matching line is found, RETURNS a two-component list. # The first element contains the instruction found (string). # The second element contains the second word on the line. # FAILS on end-of-file. local line, opcode, tail static opchar initial opchar := &lcase while line := read (uin) do line ? { if (opcode := tab (many (opchar))) == ("proc" | "lab") then { tab (many (white)) tail := tab (upto (white) | 0) break } } return [opcode, \tail] end # #-------- Pass 2 procedures -------- # procedure pass2 () # Performs a second pass over the ucode file, doing the actual tweaking. # Writes the new ucode to 'uout'. local counter, op counter := 0 while op := p2_upto ([OP_PROC]) do p2_proc (trim (op[2]), counter +:= 1) end procedure p2_addbrkp (line, last_lab, dbg_brkp, dbg_label, dbg_line, dbg_test) # Adds code for breakpoint testing. # 'line' should be the line number associated with the current ucode 'line' # instruction. # 'ltab' must be a table containing declarations of the current procedure. # 'last_lab' must be the previous highest label serial (integer). # RETURNS the new highest label serial. write (uout, "\tmark\t", ULAB_PREF, last_lab +:= 1, "\n\tpnull", "\n\tvar\t", dbg_line, "\n\tvar\t", dbg_test, "\n\tvar\t", dbg_brkp, "\n\tkeywd\tline\n\tinvoke\t2\n\tasgn\n\tgoto\t", dbg_label, "\n\tunmark\nlab ", ULAB_PREF, last_lab) return last_lab end procedure p2_addcall (ltab, dbg_label, init_label, end_label, dbg_line, dbg_name, dbg_proc, pname_decl) # Adds code for invoking the debug procedure. local decl, pname_var, vlist # Make vlist an alphabetically sorted list of identifiers: the names of # the variables which should be passed to the debugging procedure. vlist := [] every \(decl := !ltab).d_displ do put (vlist, decl.d_name) vlist := sort (vlist) # Begin writing the code. write (uout, "\tgoto\t", end_label, "\nlab ", dbg_label, "\n\tinit\t", init_label, "\n\tmark\t", init_label, "\n\tpnull\n\tvar\t", dbg_name, "\n\tpnull") every write (uout, "\tstr\t", (ltab[!vlist]).ld_cserial) pname_var := if pname_decl.d_type = D_LOCAL then pname_decl.ld_cserial else pname_decl.d_serial write (uout, "\tllist\t", *vlist, "\n\tasgn\n\tunmark\nlab ", init_label, "\n\tmark0\n\tvar\t", dbg_proc, "\n\tkeywd\tfile\n\tvar\t", dbg_line, "\n\tstr\t", pname_var, "\n\tvar\t", dbg_name) every write (uout, "\tvar\t", (ltab[!vlist]).d_serial) write (uout, "\tinvoke\t", 4 + *vlist, "\n\tunmark\nlab ", end_label, "\n\tpfail") end procedure p2_addconst (decl, last_ser) # Adds a string constant declaration containing the name of a local or constant # declaration. # 'decl' must be the declaration (record l_decl or c_decl). # 'last_ser' must be the previous highest constant serial in this procedure. # RETURNS the serial of the new constant. # SIDE EFFECT: Updates 'decl'. # Writes the new constant to the ucode output file. # NOTE: This version does not add the name if the declaration is a global and # is known to be a procedure. local serial # Omit variables which have been added by this program. (decl.d_type = D_CONST) | (/decl.ld_dbg & decl.d_code ~= LD_GLOBAL) | fail (decl.d_type = D_CONST) | (decl.d_displ := 1) serial := last_ser + 1 if decl.d_type = D_LOCAL then decl.ld_cserial := serial else decl.d_serial := serial writes (uout, "\tcon\t", serial, ",", right (CD_STRING, 6, "0"), ",", *decl.d_name) every writes (uout, ",", octal (ord (!decl.d_name))) write (uout) return serial end procedure p2_addinit (ltab, init_label) write (uout, "\tinit\t", init_label, "\n\tmark\t", init_label, "\n\tvar\t", ltab[DBG_INIT].d_serial, "\n\tinvoke\t0\n\tunmark\nlab ", init_label) end procedure p2_addlocal (pname, ltab, serial, code, name, dbg) # Adds a local declaration to a table. # 'pname' must be the current procedure name. # 'ltab' must be the table where the new declaration is stored. # See 'p2_getlocal' for details. # 'serial' must be the serial to assign to the new declaration. # 'code' must be the code, # 'name' must be the name of the new declaration. # 'dbg' may be non-null to indicate something different from a normal variable # declaration. # RETURNS the new declaration (record l_decl). # SIDE EFFECT: Writes code for the new declaration to the ucode output file. # Creates a new entry in 'ltab'. local decl, old_d # Check if the declaration already is there. if old_d := \ltab[name] then { # Check that the existing declaration is equivalent to the new. (old_d.d_code = code) | confl ("%1: conflicting declarations in procedure %2.", name, pname) return old_d } decl := l_decl (D_LOCAL) decl.d_serial := serial decl.d_code := code decl.ld_dbg := 1 ltab[decl.d_name := name] := decl write (uout, "\tlocal\t", serial, ",", right (code, 6, "0"), ",", name) return decl end procedure p2_brkp () # Scans the ucode input file for the next breakpoint location. # Ucode 'line' instructions are considered suitable breakpoint locations. # If there are several 'line' instructions with the same line number only the # last one is considered suitable. # If a location is found, RETURNS the line number of the current location. # FAILS if no suitable location is found. # This means that an 'end' instruction has been reached # When the procedure returns the 'line' instruction has been copied to the ucode # output file. # When the procedure encounters an 'end' instruction this instruction is not # copied to the ucode output file. local last_lno, line, opcode static cur_lno, opchar initial { cur_lno := NALN opchar := &lcase ++ '01' } repeat { # Read and copy until the next 'line' or 'end' instruction is found. repeat { (line := read (uin)) | intern ("p2_brkp: unexpected end of file.") line ? if tab (many (white)) & (opcode := tab (many (opchar))) then { (opcode ~== OP_END) | { last_lno := NALN break } write (uout, line) (opcode ~== OP_LINE) | { last_lno := integer (tab (0)) break } } else write (uout, line) } if last_lno = NALN then break else case cur_lno of { # Still the same line, try another one. last_lno: next # a little unstructured ... # First line found. NALN: cur_lno := last_lno # OK, this is it, stop here. default: break } } if last_lno = NALN then fail else return cur_lno :=: last_lno end procedure p2_getlocal (ltab, dstring) # Gets a local declaration from ucode representation; adds it to a table. # 'ltab' must be a table storing declarations. # Entry key is the variable name. # Entry value is an 'l_decl' record. # 'dstring' must be the ucode string defining the local. # RETURNS the serial number of the new declaration. # SIDE EFFECT: Adds an entry to 'ltab'. local decl decl := l_decl (D_LOCAL) dstring ? { decl.d_serial := integer (tab (many (&digits))) ="," decl.d_code := integer (tab (many (&digits))) ="," decl.d_name := tab (upto (white) | 0) } ltab[decl.d_name] := decl return decl.d_serial end procedure p2_newlocals (pname, ltab, last_ser, main_flag) # Adds debugging local declarations to a procedure. # 'pname' must be the procedure name (string). # 'ltab' must be a table holding local declarations; see 'p2_getlocal'. # 'last_ser' must be the last (highest) serial previously assigned. # 'main_flag' must be non-null if the current procedure is 'main'. # This will add the DBG_INIT procedure. # RETURNS the last local declaration serial. # SIDE EFFECT: Writes the new declarations to the ucode output file. # Adds the new declarations to 'ltab'. # Add the debugging init procedure if this is 'main'. /main_flag | p2_addlocal (pname, ltab, last_ser +:= 1, LD_GLOBAL, DBG_INIT) p2_addlocal (pname, ltab, last_ser +:= 1, LD_LOCAL, DBG_LINE) p2_addlocal (pname, ltab, last_ser +:= 1, LD_STATIC, DBG_NAME) p2_addlocal (pname, ltab, last_ser +:= 1, LD_GLOBAL, DBG_PROC) p2_addlocal (pname, ltab, last_ser +:= 1, LD_GLOBAL, DBG_TEST) p2_addlocal (pname, ltab, last_ser +:= 1, LD_GLOBAL, make_brkp_idf (file_root)) return last_ser end procedure p2_proc (pname) # Tweaks the ucode of a single procedure. # 'pname' must be the name of the procedure. # SIDE EFFECT: Writes tweaked ucode to the ucode output file. local dbg_brkp, dbg_label, dbg_line, dbg_name, dbg_proc, dbg_test local init_label, end_label, pname_decl local loc, first_new_const, last_conser, last_label, last_locser, line local main_flag, op static con_decl, lc_decl initial { # This is just a piece of hand optimization. con_decl := [OP_CONST, OP_DEND] lc_decl := [OP_LOCAL, OP_CONST, OP_DEND] } main_flag := pname == "main" # Go through local declarations; add some new. # See 'p2_getlocal' for documentation of the 'loc' table. loc := table () last_locser := -1 while (op := p2_upto (lc_decl))[1] == OP_LOCAL do { last_locser <:= p2_getlocal (loc, op[2]) } # Add our own locals, write them to the ucode output file. last_locser := p2_newlocals (pname, loc, last_locser, main_flag) # Go through constant declarations in order to find the maximum serial. last_conser := -1 repeat { if op[1] == OP_CONST then last_conser <:= (op[2] ? integer (tab (many (&digits)))) else break (op := p2_upto (con_decl)) | break } # Declare a constant for the procedure name. # Note that the procedure name may be hidden by a local! /loc[pname] := c_decl (D_CONST, , CD_STRING, pname) # Add new constant declarations to the ucode file. first_new_const := last_conser + 1 every last_conser := p2_addconst (!loc, last_conser) # We will soon need a new label. last_label := proc_hil[pname] # Flush the 'p2_upto' buffer, normally the 'declend' instruction. p2_upto () # If this is the 'main' procedure insert code for invoking the # initialization procedure. if \main_flag then p2_addinit (loc, ULAB_PREF || (last_label +:= 1)) # Insert breakpoint testing code. dbg_brkp := loc[make_brkp_idf (file_root)].d_serial dbg_label := ULAB_PREF || (last_label +:= 1) dbg_line := loc[DBG_LINE].d_serial dbg_test := loc[DBG_TEST].d_serial while last_label := p2_addbrkp (p2_brkp (), last_label, dbg_brkp, dbg_label, dbg_line, dbg_test) # Write the debug invocation code. init_label := ULAB_PREF || (last_label +:= 1) end_label := ULAB_PREF || (last_label +:= 1) dbg_name := loc[DBG_NAME].d_serial dbg_proc := loc[DBG_PROC].d_serial pname_decl := loc[pname] p2_addcall (loc, dbg_label, init_label, end_label, dbg_line, dbg_name, dbg_proc, pname_decl) # Add an 'end' instruction swallowed by 'p2_brkp'. write (uout, "\t", OP_END) end procedure p2_upto (op) # Scans the ucode file, looking for the next line containing an interesting # op-code. # Copies non-matching lines to the new ucode file (if non-null) # 'op' must be a list of the interesting op-code(s), or null. # If a matching line is found, RETURNS a list of two elements. # The first element contains the op-code, the second element the tail of the # instruction (excluding any leading white space). # FAILS on end-of-file. # FLUSHING THE BUFFER: # If the procedure is invoked with null 'op' any uncopied lines are written to # the ucode output file; the procedure fails. # NOTE: The procedure is used occasionally in pass 1, where there is no 'uout' # file. # This is the reason 'uout' is checked for existence (otherwise ucode will # appear on standard output). local opcode, tail static new_line, opchar, old_line initial opchar := &lcase ++ '01' write (\uout, \new_line) new_line := &null \op | fail repeat { old_line := new_line (new_line := read (uin)) | fail new_line ? { tab (many (white)) if (opcode := tab (many (opchar))) == !op then { tab (many (white)) tail := tab (0) break } else write (\uout, new_line) } } return [opcode, tail] end # #-------- '.u2' tweaking ----------- # procedure u2tweak () # Tweaks a '.u2' file, which means: # Check the Icon version number; # insert 'link' commands to the debugging run-time and to the init procedure. local hitcount, op (op := p2_upto ([OP_VERSION])) | { note ("Surprising absence of 'version' in .u2 file...") fail } (ICON_VER_LO <<= op[2] <<= ICON_VER_HI) | note ("WARNING: %1 is tested only for Icon versions '%2'-'%3', found '%4'.", PROGNAME, ICON_VER_LO, ICON_VER_HI, op[2]) hitcount := 0 while (op := p2_upto ([OP_LINK, OP_GLOBAL]))[1] == OP_LINK do if op[2] == DBG_RUN_TIME then hitcount +:= 1 if hitcount = 0 then { write (uout, OP_LINK, "\t", DBG_RUN_TIME) write (uout, OP_LINK, "\t", init_file) } p2_upto () while write (uout, read (uin)) end # #-------- General message handling and other utilities -------- # procedure confl (msg, parm[]) # Writes a conflict message and stops the program with nonzero exit code. message ("[CONFLICT] ", subst (msg, parm)) message ("*** ", PROGNAME, " stops with failure.") stop () end procedure cre_init (f) # Creates initialization code. # 'f' must be a file open for output. local map, version version := (PROGRAM_VERSION ? (tab (upto (&digits)), tab (many (&digits++'.')))) every write (f, "global ", (PROG_VERSION_VAR | DBG_TEST | DBG_FILE_MAP)) every write (f, "global ", make_brkp_idf ((!file_map).fm_ucode)) write (f, "\nprocedure ", DBG_INIT, " ()\n\t", PROG_VERSION_VAR, " := \"", version, "\"\n\t", DBG_TEST, " := member") every write (f, "\t", make_brkp_idf ((!file_map).fm_ucode), " := set ()") write (f, "\t", DBG_FILE_MAP, " := table ()") every map := !file_map do write (f, "\t", DBG_FILE_MAP, "[\"", map.fm_source, "\"] := ", make_brkp_idf (map.fm_ucode)) write (f, "\t", DBG_PROC, " ()\nend") end procedure fld_adj (str) # Part of 'subst' format string parsing. # 'str' must be a parameter string identified by the beginning part of a # placeholder ('%n'). # This procedure checks if the placeholder contains a fixed field width # specifier. # A fixed field specifier begins with '<' or '<' and continues with the field # width expressed as a decimal literal. # RETURNS 'str' possibly inserted in a fixed width field. local just, init_p, res, wid static fwf initial fwf := '<>' init_p := &pos if (just := if ="<" then left else if =">" then right) & (wid := integer (tab (many (&digits)))) then res := just (str, wid) else { res := str &pos := init_p } return res end procedure intern (msg, parm[]) # Writes an internal conflict message and stops the program with nonzero exit # code. message ("*** INTERNAL: ", subst (msg, parm)) message ("*** ", PROGNAME, " stops with failure.") stop () end procedure make_brkp_idf (ucode_root) # RETURNS an identifier which should be used to hold the breakpoints of an # ucode file whose root name is 'ucode_root'. return DBG_BRKP1 || ucode_root || DBG_BRKP2 end procedure message (parm[]) # Writes any number of strings to the message file. every writes (msgout, !parm) write (msgout) end procedure note (msg, parm[]) # Writes a note message. message ("[NOTE] ", subst (msg, parm)) end procedure octal (i) # RETURNS the 'i' integer in the form of an octal literal. static digits local s, d initial digits := string (&digits) if i = 0 then return "0" s := "" while i > 0 do { d := i % 8 if d > 9 then d := digits[d + 1] s := d || s i /:= 8 } return s end procedure subst (msg, parm) # Substitutes parameters in a message template. # 'msg' must be a message template (string). # 'parm' must be a list of parameters (list of string-convertible), or null. # It may also be a string. local esc, res, sub static p_digit initial p_digit := '123456789' \parm | return msg parm := [string (parm)] res := "" msg ? until pos (0) do { res ||:= tab (upto ('%\\') | 0) if ="%" then res ||:= { if any (p_digit) then { sub := (\parm[integer (move (1))] | "") fld_adj (sub) } else if any ('%') then move (1) else "" } else if ="\\" then res ||:= case esc := move (1) of { "n": "\n" "t": "\t" default: esc } } return res end