summaryrefslogtreecommitdiff
path: root/ipl/progs/ipp.icn
blob: 16c8a44cb47585dbf02d19355965cdf650fe3c9a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
############################################################################
#
#	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 <filename>' 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