]> git.xonotic.org Git - xonotic/xonotic.git/blob - misc/tools/progs-analyzer.pl
better errors
[xonotic/xonotic.git] / misc / tools / progs-analyzer.pl
1 use strict;
2 use warnings;
3 use Digest::SHA;
4 use Carp;
5
6 sub id()
7 {
8         return sub { $_[0]; };
9 }
10
11 sub signed($)
12 {
13         my ($bits) = @_;
14         return sub { $_[0] >= (2**($bits-1)) ? $_[0]-(2**$bits) : $_[0]; };
15 }
16
17 use constant OPCODE_E => [qw[
18         DONE
19         MUL_F MUL_V MUL_FV MUL_VF
20         DIV_F
21         ADD_F ADD_V
22         SUB_F SUB_V
23         EQ_F EQ_V EQ_S EQ_E EQ_FNC
24         NE_F NE_V NE_S NE_E NE_FNC
25         LE GE LT GT
26         LOAD_F LOAD_V LOAD_S LOAD_ENT LOAD_FLD LOAD_FNC
27         ADDRESS
28         STORE_F STORE_V STORE_S STORE_ENT STORE_FLD STORE_FNC
29         STOREP_F STOREP_V STOREP_S STOREP_ENT STOREP_FLD STOREP_FNC
30         RETURN
31         NOT_F NOT_V NOT_S NOT_ENT NOT_FNC
32         IF IFNOT
33         CALL0 CALL1 CALL2 CALL3 CALL4 CALL5 CALL6 CALL7 CALL8
34         STATE
35         GOTO
36         AND OR
37         BITAND BITOR
38 ]];
39 use constant ETYPE_E => [qw[
40         void
41         string
42         float
43         vector
44         entity
45         field
46         function
47         pointer
48 ]];
49 use constant DEF_SAVEGLOBAL => 32768;
50 sub typesize($)
51 {
52         my ($type) = @_;
53         return 3 if $type eq 'vector';
54         return 1;
55 }
56
57 sub checkop($)
58 {
59         my ($op) = @_;
60         if($op =~ /^IF.*_V$/)
61         {
62                 return { a => 'inglobalvec', b => 'ipoffset', isjump => 'b', isconditional => 1 };
63         }
64         if($op =~ /^IF/)
65         {
66                 return { a => 'inglobal', b => 'ipoffset', isjump => 'b', isconditional => 1 };
67         }
68         if($op eq 'GOTO')
69         {
70                 return { a => 'ipoffset', isjump => 'a', isconditional => 0 };
71         }
72         if($op =~ /^ADD_V$|^SUB_V$/)
73         {
74                 return { a => 'inglobalvec', b => 'inglobalvec', c => 'outglobalvec' };
75         }
76         if($op =~ /^MUL_V$|^EQ_V$|^NE_V$/)
77         {
78                 return { a => 'inglobalvec', b => 'inglobalvec', c => 'outglobal' };
79         }
80         if($op eq 'MUL_FV')
81         {
82                 return { a => 'inglobal', b => 'inglobalvec', c => 'outglobalvec' };
83         }
84         if($op eq 'MUL_VF')
85         {
86                 return { a => 'inglobalvec', b => 'inglobal', c => 'outglobalvec' };
87         }
88         if($op eq 'LOAD_V')
89         {
90                 return { a => 'inglobal', b => 'inglobal', c => 'outglobalvec' };
91         }
92         if($op =~ /^NOT_V/)
93         {
94                 return { a => 'inglobalvec', c => 'outglobal' };
95         }
96         if($op =~ /^NOT_/)
97         {
98                 return { a => 'inglobal', c => 'outglobal' };
99         }
100         if($op eq 'STOREP_V')
101         {
102                 return { a => 'inglobalvec', b => 'inglobal' };
103         }
104         if($op eq 'STORE_V')
105         {
106                 return { a => 'inglobalvec', b => 'outglobalvec' };
107         }
108         if($op =~ /^STOREP_/)
109         {
110                 return { a => 'inglobal', b => 'inglobal' };
111         }
112         if($op =~ /^STORE_/)
113         {
114                 return { a => 'inglobal', b => 'outglobal' };
115         }
116         if($op =~ /^CALL/)
117         {
118                 return { a => 'inglobalfunc', iscall => 1 };
119         }
120         if($op =~ /^DONE$|^RETURN$/)
121         {
122                 return { a => 'inglobalvec', isreturn => 1 };
123         }
124         return { a => 'inglobal', b => 'inglobal', c => 'outglobal' };
125 }
126
127 use constant TYPES => {
128         int => ['V', 4, signed 32],
129         ushort => ['v', 2, id],
130         short => ['v', 2, signed 16],
131         opcode => ['v', 2, sub { OPCODE_E->[$_[0]] or do { warn "Invalid opcode: $_[0]"; "INVALID#$_[0]"; }; }],
132         float => ['f', 4, id],
133         uchar8 => ['a8', 8, sub { [unpack 'C8', $_[0]] }],
134         global => ['i', 4, sub { { int => $_[0], float => unpack "f", pack "L", $_[0] }; }],
135         deftype => ['v', 2, sub { { type => ETYPE_E->[$_[0] & ~DEF_SAVEGLOBAL], save => !!($_[0] & DEF_SAVEGLOBAL) }; }],
136 };
137
138 use constant DPROGRAMS_T => [
139         [int => 'version'],
140         [int => 'crc'],
141         [int => 'ofs_statements'],
142         [int => 'numstatements'],
143         [int => 'ofs_globaldefs'],
144         [int => 'numglobaldefs'],
145         [int => 'ofs_fielddefs'],
146         [int => 'numfielddefs'],
147         [int => 'ofs_functions'],
148         [int => 'numfunctions'],
149         [int => 'ofs_strings'],
150         [int => 'numstrings'],
151         [int => 'ofs_globals'],
152         [int => 'numglobals'],
153         [int => 'entityfields']
154 ];
155
156 use constant DSTATEMENT_T => [
157         [opcode => 'op'],
158         [short => 'a'],
159         [short => 'b'],
160         [short => 'c']
161 ];
162
163 use constant DDEF_T => [
164         [deftype => 'type'],
165         [ushort => 'ofs'],
166         [int => 's_name']
167 ];
168
169 use constant DGLOBAL_T => [
170         [global => 'v'],
171 ];
172
173 use constant DFUNCTION_T => [
174         [int => 'first_statement'],
175         [int => 'parm_start'],
176         [int => 'locals'],
177         [int => 'profile'],
178         [int => 's_name'],
179         [int => 's_file'],
180         [int => 'numparms'],
181         [uchar8 => 'parm_size'],
182 ];
183
184 sub get_section($$$)
185 {
186         my ($fh, $start, $len) = @_;
187         seek $fh, $start, 0
188                 or die "seek: $!";
189         $len == read $fh, my $buf, $len
190                 or die "short read from $start length $len (malformed progs header)";
191         return $buf;
192 }
193
194 sub parse_section($$$$$)
195 {
196         my ($fh, $struct, $start, $len, $cnt) = @_;
197
198         my $itemlen = 0;
199         $itemlen += TYPES->{$_->[0]}->[1]
200                 for @$struct;
201         my $packspec = join '', map { TYPES->{$_->[0]}->[0]; } @$struct;
202         my @packnames = map { $_->[1]; } @$struct;
203
204         $len = $cnt * $itemlen
205                 if not defined $len and defined $cnt;
206         $cnt = int($len / $itemlen)
207                 if not defined $cnt and defined $len;
208         die "Invalid length specification"
209                 unless defined $len and defined $cnt and $len == $cnt * $itemlen;
210         die "Invalid length specification in scalar context"
211                 unless wantarray or $cnt == 1;
212
213         seek $fh, $start, 0
214                 or die "seek: $!";
215         my @out = map
216         {
217                 $itemlen == read $fh, my $buf, $itemlen
218                         or die "short read from $start length $cnt * $itemlen $(malformed progs header)";
219                 my %h = ();
220                 @h{@packnames} = unpack $packspec, $buf;
221                 $h{$_->[1]} = TYPES->{$_->[0]}->[2]->($h{$_->[1]})
222                         for @$struct;
223                 \%h;
224         }
225         0..($cnt-1);
226         return @out
227                 if wantarray;
228         return $out[0];
229 }
230
231 sub nfa_default_state_checker()
232 {
233         my %seen;
234         return sub
235         {
236                 my ($ip, $state) = @_;
237                 return $seen{"$ip $state"}++;
238         };
239 }
240
241 sub run_nfa($$$$$$)
242 {
243         my ($progs, $ip, $state, $copy_handler, $state_checker, $instruction_handler) = @_;
244
245         my $statements = $progs->{statements};
246
247         my $nfa;
248         $nfa = sub
249         {
250                 no warnings 'recursion';
251
252                 my ($ip, $state) = @_;
253                 my $ret = 0;
254
255                 for(;;)
256                 {
257                         return $ret
258                                 if $state_checker->($ip, $state);
259
260                         my $s = $statements->[$ip];
261                         my $c = checkop $s->{op};
262
263                         if(($ret = $instruction_handler->($ip, $state, $s, $c)))
264                         {
265                                 # abort execution
266                                 last;
267                         }
268
269                         if($c->{isreturn})
270                         {
271                                 last;
272                         }
273                         elsif($c->{iscall})
274                         {
275                                 my $func = $s->{a};
276                                 my $funcid = $progs->{globals}[$func]{v}{int};
277                                 last
278                                         if $progs->{error_func}{$funcid};
279                                 $ip += 1;
280                         }
281                         elsif($c->{isjump})
282                         {
283                                 if($c->{isconditional})
284                                 {
285                                         if(rand 2)
286                                         {
287                                                 if(($ret = $nfa->($ip+$s->{$c->{isjump}}, $copy_handler->($state))) < 0)
288                                                 {
289                                                         last;
290                                                 }
291                                                 $ip += 1;
292                                         }
293                                         else
294                                         {
295                                                 $nfa->($ip+1, $copy_handler->($state));
296                                                 $ip += $s->{$c->{isjump}};
297                                         }
298                                 }
299                                 else
300                                 {
301                                         $ip += $s->{$c->{isjump}};
302                                 }
303                         }
304                         else
305                         {
306                                 $ip += 1;
307                         }
308                 }
309
310                 return $ret;
311         };
312
313         $nfa->($ip, $copy_handler->($state));
314 }
315
316 sub get_constant($$)
317 {
318         my ($progs, $g) = @_;
319         if($g->{int} == 0)
320         {
321                 return 0;
322         }
323         elsif($g->{int} > 0 && $g->{int} < 8388608)
324         {
325                 if($g->{int} < length $progs->{strings} && $g->{int} > 0)
326                 {
327                         return str($progs->{getstring}->($g->{int}));
328                 }
329                 else
330                 {
331                         return $g->{int} . "i";
332                 }
333         }
334         else
335         {
336                 return $g->{float};
337         }
338 }
339
340 use constant PRE_MARK_STATEMENT => "";
341 use constant POST_MARK_STATEMENT => "";
342 use constant PRE_MARK_OPERAND => "*** ";
343 use constant POST_MARK_OPERAND => " ***";
344
345 use constant INSTRUCTION_FORMAT => "%8s %3s | %-12s ";
346 use constant OPERAND_FORMAT => "%s";
347 use constant OPERAND_SEPARATOR => ", ";
348 use constant INSTRUCTION_SEPARATOR => "\n";
349
350 sub str($)
351 {
352         my ($str) = @_;
353         $str =~ s/[\000-\037\\\"\177-\377]/sprintf "\\%03o", ord $&/ge;
354         return "\"$str\"";
355 }
356
357 sub disassemble_function($$;$)
358 {
359         my ($progs, $func, $highlight) = @_;
360
361         print "$func->{debugname}:\n";
362
363         my $initializer = sub
364         {
365                 my ($ofs) = @_;
366                 my $g = get_constant($progs, $progs->{globals}[$ofs]{v});
367                 print " = $g"
368                         if defined $g;
369         };
370
371         printf INSTRUCTION_FORMAT, '', '', '.PARM_START';
372         printf OPERAND_FORMAT, "$func->{parm_start}";
373         print INSTRUCTION_SEPARATOR;
374
375         printf INSTRUCTION_FORMAT, '', '', '.LOCALS';
376         printf OPERAND_FORMAT, "$func->{locals}";
377         print INSTRUCTION_SEPARATOR;
378
379         my %override_locals = ();
380         my $p = $func->{parm_start};
381         for(0..($func->{numparms}-1))
382         {
383                 $override_locals{$p} //= "argv_$_";
384                 for my $comp(0..($func->{parm_size}[$_]-1))
385                 {
386                         $override_locals{$p} //= "argv_$_\[$comp]";
387                         ++$p;
388                 }
389                 printf INSTRUCTION_FORMAT, '', '', '.ARG';
390                 printf OPERAND_FORMAT, "argv_$_";
391                 print OPERAND_SEPARATOR;
392                 printf OPERAND_FORMAT, $func->{parm_size}[$_];
393                 print INSTRUCTION_SEPARATOR;
394         }
395         for($func->{parm_start}..($func->{parm_start} + $func->{locals} - 1))
396         {
397                 next
398                         if exists $override_locals{$_};
399                 $override_locals{$_} = "local_$_";
400
401                 printf INSTRUCTION_FORMAT, '', '', '.LOCAL';
402                 printf OPERAND_FORMAT, "local_$_";
403                 $initializer->($_);
404                 print INSTRUCTION_SEPARATOR;
405         }
406
407         my $getname = sub
408         {
409                 my ($ofs) = @_;
410                 return $override_locals{$ofs}
411                         if exists $override_locals{$ofs};
412                 my $def = $progs->{globaldef_byoffset}->($ofs);
413                 return $def->{debugname};
414         };
415
416         my $operand = sub
417         {
418                 my ($ip, $type, $operand) = @_;
419                 if($type eq 'inglobal')
420                 {
421                         my $name = $getname->($operand);
422                         printf OPERAND_FORMAT, "$name";
423                 }
424                 elsif($type eq 'outglobal')
425                 {
426                         my $name = $getname->($operand);
427                         printf OPERAND_FORMAT, "&$name";
428                 }
429                 elsif($type eq 'inglobalvec')
430                 {
431                         my $name = $getname->($operand);
432                         printf OPERAND_FORMAT, "$name\[\]";
433                 }
434                 elsif($type eq 'outglobalvec')
435                 {
436                         my $name = $getname->($operand);
437                         printf OPERAND_FORMAT, "&$name\[\]";
438                 }
439                 elsif($type eq 'inglobalfunc')
440                 {
441                         my $name = $getname->($operand);
442                         printf OPERAND_FORMAT, "$name()";
443                 }
444                 elsif($type eq 'ipoffset')
445                 {
446                         printf OPERAND_FORMAT, "@{[$ip + $operand]}" . sprintf ' ($%+d)', $operand;
447                 }
448                 else
449                 {
450                         die "unknown type: $type";
451                 }
452         };
453
454         my $statements = $func->{statements};
455         my $come_from = $func->{come_from};
456
457         my $ipprev = undef;
458         for my $ip(sort { $a <=> $b } keys %$statements)
459         {
460                 if($ip == $func->{first_statement})
461                 {
462                         printf INSTRUCTION_FORMAT, $ip, '', '.ENTRY';
463                         print INSTRUCTION_SEPARATOR;
464                 }
465                 if(defined $ipprev && $ip != $ipprev + 1)
466                 {
467                         printf INSTRUCTION_FORMAT, $ip, '', '.SKIP';
468                         printf OPERAND_FORMAT, $ip - $ipprev - 1;
469                         print INSTRUCTION_SEPARATOR;
470                 }
471                 if(my $cf = $come_from->{$ip})
472                 {
473                         printf INSTRUCTION_FORMAT, $ip, '', '.XREF';
474                         my $cnt = 0;
475                         for(sort { $a <=> $b } keys %$cf)
476                         {
477                                 print OPERAND_SEPARATOR
478                                         if $cnt++;
479                                 printf OPERAND_FORMAT, ($cf->{$_} ? 'c' : 'j') . $_ . sprintf ' ($%+d)', $_ - $ip;
480                         }
481                         print INSTRUCTION_SEPARATOR;
482                 }
483
484                 my $op = $progs->{statements}[$ip]{op};
485                 my $ipt = $progs->{statements}[$ip];
486                 my $opprop = checkop $op;
487
488                 if($highlight and $highlight->{$ip})
489                 {
490                         for(values %{$highlight->{$ip}})
491                         {
492                                 for(@$_)
493                                 {
494                                         print PRE_MARK_STATEMENT;
495                                         printf INSTRUCTION_FORMAT, '', '<!>', '.WARN';
496                                         printf OPERAND_FORMAT, "$_ (in $func->{debugname})";
497                                         print INSTRUCTION_SEPARATOR;
498                                 }
499                         }
500                 }
501
502                 print PRE_MARK_STATEMENT
503                         if $highlight and $highlight->{$ip};
504
505                 my $showip = $opprop->{isjump};
506                 printf INSTRUCTION_FORMAT, $showip ? $ip : '', $highlight->{$ip} ? '<!>' : '', $op;
507
508                 my $cnt = 0;
509                 for my $o(qw(a b c))
510                 {
511                         next
512                                 if not defined $opprop->{$o};
513                         print OPERAND_SEPARATOR
514                                 if $cnt++;
515                         print PRE_MARK_OPERAND
516                                 if $highlight and $highlight->{$ip} and $highlight->{$ip}{$o};
517                         $operand->($ip, $opprop->{$o}, $ipt->{$o});
518                         print POST_MARK_OPERAND
519                                 if $highlight and $highlight->{$ip} and $highlight->{$ip}{$o};
520                 }
521
522                 print POST_MARK_STATEMENT
523                         if $highlight and $highlight->{$ip};
524
525                 print INSTRUCTION_SEPARATOR;
526         }
527 }
528
529 sub find_uninitialized_locals($$)
530 {
531         my ($progs, $func) = @_;
532
533
534         return
535                 if $func->{first_statement} < 0; # builtin
536
537         print STDERR "Checking $func->{debugname}...\n";
538
539         my $p = $func->{parm_start};
540         for(0..($func->{numparms}-1))
541         {
542                 $p += $func->{parm_size}[$_];
543         }
544
545         use constant WATCHME_R => 1;
546         use constant WATCHME_W => 2;
547         use constant WATCHME_X => 4;
548         use constant WATCHME_T => 8;
549         my %watchme = map { $_ => WATCHME_X } ($func->{parm_start} .. ($func->{parm_start} + $func->{locals} - 1));
550
551         for(keys %{$progs->{temps}})
552         {
553                 next
554                         if exists $watchme{$_};
555                 if($progs->{temps}{$_})
556                 {
557                         # shared temp
558                         $watchme{$_} = WATCHME_T | WATCHME_X
559                 }
560                 else
561                 {
562                         # unique temp
563                         $watchme{$_} = WATCHME_X
564                 }
565         }
566
567         $watchme{$_} |= WATCHME_R
568                 for keys %{$func->{globals_read}};
569         $watchme{$_} |= WATCHME_W
570                 for keys %{$func->{globals_written}};
571
572         my %write_places = ();
573         for my $ofs(keys %{$func->{globals_written}})
574         {
575                 next
576                         unless exists $watchme{$ofs} and $watchme{$ofs} & WATCHME_X;
577                 for my $ip(keys %{$func->{globals_written}{$ofs}})
578                 {
579                         for my $op(keys %{$func->{globals_written}{$ofs}{$ip}})
580                         {
581                                 push @{$write_places{$ip}{$op}}, $ofs;
582                         }
583                 }
584         }
585
586         for(keys %watchme)
587         {
588                 delete $watchme{$_}
589                         if ($watchme{$_} & (WATCHME_R | WATCHME_W | WATCHME_X)) != (WATCHME_R | WATCHME_W | WATCHME_X);
590         }
591
592         return
593                 if not keys %watchme;
594
595         for(keys %watchme)
596         {
597                 $watchme{$_} = {
598                         flags => $watchme{$_},
599                         valid => [0, undef, undef]
600                 };
601         }
602
603         # mark parameters as initialized
604         for($func->{parm_start} .. ($p-1))
605         {
606                 $watchme{$_}{valid} = [1, undef, undef]
607                         if defined $watchme{$_};
608         }
609
610         my %warned = ();
611         my %ip_seen = ();
612         run_nfa $progs, $func->{first_statement}, \%watchme,
613                 sub {
614                         my ($h) = @_;
615                         return { map { $_ => { %{$h->{$_}} } } keys %$h };
616                 },
617                 sub {
618                         my ($ip, $state) = @_;
619
620                         my $s = $ip_seen{$ip};
621                         if($s)
622                         {
623                                 # if $state is stronger or equal to $s, return 1
624
625                                 for(keys %$state)
626                                 {
627                                         if($state->{$_}{valid}[0] < $s->{$_})
628                                         {
629                                                 # The current state is LESS valid than the previously run one. We NEED to run this.
630                                                 # The saved state can safely become the intersection [citation needed].
631                                                 for(keys %$state)
632                                                 {
633                                                         $s->{$_} = $state->{$_}{valid}[0]
634                                                                 if $state->{$_}{valid}[0] < $s->{$_};
635                                                 }
636                                                 return 0;
637                                         }
638                                 }
639                                 # if we get here, $state is stronger or equal. No need to try it.
640                                 return 1;
641                         }
642                         else
643                         {
644                                 # Never seen this IP yet.
645                                 $ip_seen{$ip} = { map { ($_ => $state->{$_}{valid}[0]); } keys %$state };
646                                 return 0;
647                         }
648                 },
649                 sub {
650                         my ($ip, $state, $s, $c) = @_;
651                         my $op = $s->{op};
652
653                         # QCVM BUG: RETURN always takes vector, there is no float equivalent
654                         my $return_hack = $c->{isreturn} // 0;
655
656                         if($op eq 'STORE_V')
657                         {
658                                 # COMPILER BUG of QCC: params are always copied using STORE_V
659                                 if($s->{b} >= 4 && $s->{b} < 28) # parameter range
660                                 {
661                                         $return_hack = 1;
662                                 }
663                         }
664
665                         for(qw(a b c))
666                         {
667                                 my $type = $c->{$_};
668                                 next
669                                         unless defined $type;
670
671                                 my $ofs = $s->{$_};
672
673                                 my $read = sub
674                                 {
675                                         my ($ofs) = @_;
676                                         ++$return_hack
677                                                 if $return_hack;
678                                         return
679                                                 if not exists $state->{$ofs};
680                                         my $valid = $state->{$ofs}{valid};
681                                         if($valid->[0] == 0)
682                                         {
683                                                 # COMPILER BUG of FTEQCC: AND and OR may take uninitialized as second argument (logicops)
684                                                 if($return_hack <= 2 and ($op ne 'OR' && $op ne 'AND' || $_ ne 'b'))
685                                                 {
686                                                         push @{$warned{$ip}{$_}}, "Use of uninitialized value";
687                                                 }
688                                         }
689                                         elsif($valid->[0] < 0)
690                                         {
691                                                 # COMPILER BUG of FTEQCC: AND and OR may take uninitialized as second argument (logicops)
692                                                 if($return_hack <= 2 and ($op ne 'OR' && $op ne 'AND' || $_ ne 'b'))
693                                                 {
694                                                         push @{$warned{$ip}{$_}}, "Use of temporary across CALL";
695                                                 }
696                                         }
697                                         else
698                                         {
699                                                 # it's VALID
700                                                 if(defined $valid->[1])
701                                                 {
702                                                         delete $write_places{$valid->[1]}{$valid->[2]};
703                                                 }
704                                         }
705                                 };
706                                 my $write = sub
707                                 {
708                                         my ($ofs) = @_;
709                                         $state->{$ofs}{valid} = [1, $ip, $_]
710                                                 if exists $state->{$ofs};
711                                 };
712
713                                 if($type eq 'inglobal' || $type eq 'inglobalfunc')
714                                 {
715                                         $read->($ofs);
716                                 }
717                                 elsif($type eq 'inglobalvec')
718                                 {
719                                         $read->($ofs);
720                                         $read->($ofs+1);
721                                         $read->($ofs+2);
722                                 }
723                                 elsif($type eq 'outglobal')
724                                 {
725                                         $write->($ofs);
726                                 }
727                                 elsif($type eq 'outglobalvec')
728                                 {
729                                         $write->($ofs);
730                                         $write->($ofs+1);
731                                         $write->($ofs+2);
732                                 }
733                         }
734                         if($c->{iscall})
735                         {
736                                 # builtin calls may clobber stuff
737                                 my $func = $s->{a};
738                                 my $funcid = $progs->{globals}[$func]{v}{int};
739                                 my $funcobj = $progs->{functions}[$funcid];
740                                 if(!$funcobj || $funcobj->{first_statement} >= 0)
741                                 {
742                                         # invalidate temps
743                                         for(values %$state)
744                                         {
745                                                 if($_->{flags} & WATCHME_T)
746                                                 {
747                                                         $_->{valid} = [-1, undef, undef];
748                                                 }
749                                         }
750                                 }
751                         }
752
753                         return 0;
754                 };
755
756         for my $ip(keys %write_places)
757         {
758                 for my $operand(keys %{$write_places{$ip}})
759                 {
760                         # TODO verify it
761                         my %left = map { $_ => 1 } @{$write_places{$ip}{$operand}};
762                         my $isread = 0;
763
764                         my %writeplace_seen = ();
765                         run_nfa $progs, $ip+1, \%left,
766                                 sub
767                                 {
768                                         return { %{$_[0]} };
769                                 },
770                                 sub
771                                 {
772                                         my ($ip, $state) = @_;
773                                         return $writeplace_seen{"$ip " . join " ", sort keys %$state}++;
774                                 },
775                                 sub
776                                 {
777                                         my ($ip, $state, $s, $c) = @_;
778                                         for(qw(a b c))
779                                         {
780                                                 my $type = $c->{$_};
781                                                 next
782                                                         unless defined $type;
783
784                                                 my $ofs = $s->{$_};
785                                                 if($type eq 'inglobal' || $type eq 'inglobalfunc')
786                                                 {
787                                                         if($state->{$ofs})
788                                                         {
789                                                                 $isread = 1;
790                                                                 return -1; # exit TOTALLY
791                                                         }
792                                                 }
793                                                 elsif($type eq 'inglobalvec')
794                                                 {
795                                                         if($state->{$ofs} || $state->{$ofs+1} || $state->{$ofs+2})
796                                                         {
797                                                                 $isread = 1;
798                                                                 return -1; # exit TOTALLY
799                                                         }
800                                                 }
801                                                 elsif($type eq 'outglobal')
802                                                 {
803                                                         delete $state->{$ofs};
804                                                         return 1
805                                                                 if !%$state;
806                                                 }
807                                                 elsif($type eq 'outglobalvec')
808                                                 {
809                                                         delete $state->{$ofs};
810                                                         delete $state->{$ofs+1};
811                                                         delete $state->{$ofs+2};
812                                                         return 1
813                                                                 if !%$state;
814                                                 }
815                                         }
816                                         return 0;
817                                 };
818
819                         if(!$isread)
820                         {
821                                 push @{$warned{$ip}{$operand}}, "Value is never used";
822                         }
823                 }
824         }
825         
826         disassemble_function($progs, $func, \%warned)
827                 if keys %warned;
828 }
829
830 use constant DEFAULTGLOBALS => [
831         "OFS_NULL",
832         "OFS_RETURN",
833         "OFS_RETURN[1]",
834         "OFS_RETURN[2]",
835         "OFS_PARM0",
836         "OFS_PARM0[1]",
837         "OFS_PARM0[2]",
838         "OFS_PARM1",
839         "OFS_PARM1[1]",
840         "OFS_PARM1[2]",
841         "OFS_PARM2",
842         "OFS_PARM2[1]",
843         "OFS_PARM2[2]",
844         "OFS_PARM3",
845         "OFS_PARM3[1]",
846         "OFS_PARM3[2]",
847         "OFS_PARM4",
848         "OFS_PARM4[1]",
849         "OFS_PARM4[2]",
850         "OFS_PARM5",
851         "OFS_PARM5[1]",
852         "OFS_PARM5[2]",
853         "OFS_PARM6",
854         "OFS_PARM6[1]",
855         "OFS_PARM6[2]",
856         "OFS_PARM7",
857         "OFS_PARM7[1]",
858         "OFS_PARM7[2]"
859 ];
860
861 sub defaultglobal($)
862 {
863         my ($ofs) = @_;
864         if($ofs < @{(DEFAULTGLOBALS)})
865         {
866                 return { ofs => $ofs, s_name => undef, debugname => DEFAULTGLOBALS->[$ofs], type => undef };
867         }
868         return { ofs => $ofs, s_name => undef, debugname => "<undefined>\@$ofs", type => undef };
869 }
870
871 sub detect_constants($)
872 {
873         my ($progs) = @_;
874         use constant GLOBALFLAG_R => 1; # read
875         use constant GLOBALFLAG_W => 2; # written
876         use constant GLOBALFLAG_S => 4; # saved
877         use constant GLOBALFLAG_I => 8; # initialized
878         use constant GLOBALFLAG_N => 16; # named
879         use constant GLOBALFLAG_Q => 32; # unique to function
880         use constant GLOBALFLAG_U => 64; # unused
881         use constant GLOBALFLAG_P => 128; # possibly parameter passing
882         my @globalflags = (GLOBALFLAG_Q | GLOBALFLAG_U) x @{$progs->{globals}};
883
884         for(@{$progs->{functions}})
885         {
886                 for(keys %{$_->{globals_used}})
887                 {
888                         if($globalflags[$_] & GLOBALFLAG_U)
889                         {
890                                 $globalflags[$_] &= ~GLOBALFLAG_U;
891                         }
892                         elsif($globalflags[$_] & GLOBALFLAG_Q)
893                         {
894                                 $globalflags[$_] &= ~GLOBALFLAG_Q;
895                         }
896                 }
897                 $globalflags[$_] |= GLOBALFLAG_R
898                         for keys %{$_->{globals_read}};
899                 $globalflags[$_] |= GLOBALFLAG_W
900                         for keys %{$_->{globals_written}};
901                 next
902                         if $_->{first_statement} < 0;
903                 for my $ip($_->{first_statement} .. (@{$progs->{statements}}-1))
904                 {
905                         my $s = $progs->{statements}[$ip];
906                         if($s->{op} eq 'STORE_V')
907                         {
908                                 $globalflags[$s->{a}] |= GLOBALFLAG_P
909                                         if $s->{b} >= $_->{parm_start} and $s->{b} < $_->{parm_start} + $_->{locals};
910                                 $globalflags[$s->{a}+1] |= GLOBALFLAG_P
911                                         if $s->{b}+1 >= $_->{parm_start} and $s->{b}+1 < $_->{parm_start} + $_->{locals};
912                                 $globalflags[$s->{a}+2] |= GLOBALFLAG_P
913                                         if $s->{b}+2 >= $_->{parm_start} and $s->{b}+2 < $_->{parm_start} + $_->{locals};
914                         }
915                         elsif($s->{op} =~ /^STORE_/)
916                         {
917                                 $globalflags[$s->{a}] |= GLOBALFLAG_P
918                                         if $s->{b} >= $_->{parm_start} and $s->{b} < $_->{parm_start} + $_->{locals};
919                         }
920                         else
921                         {
922                                 last;
923                         }
924                 }
925         }
926
927         # parameter passing globals are only ever used in STORE_ instructions
928         for my $s(@{$progs->{statements}})
929         {
930                 next
931                         if $s->{op} =~ /^STORE_/;
932
933                 my $c = checkop $s->{op};
934
935                 for(qw(a b c))
936                 {
937                         my $type = $c->{$_};
938                         next
939                                 unless defined $type;
940
941                         my $ofs = $s->{$_};
942                         if($type eq 'inglobal' || $type eq 'inglobalfunc' || $type eq 'outglobal')
943                         {
944                                 $globalflags[$ofs] &= ~GLOBALFLAG_P;
945                         }
946                         if($type eq 'inglobalvec' || $type eq 'outglobalvec')
947                         {
948                                 $globalflags[$ofs] &= ~GLOBALFLAG_P;
949                                 $globalflags[$ofs+1] &= ~GLOBALFLAG_P;
950                                 $globalflags[$ofs+2] &= ~GLOBALFLAG_P;
951                         }
952                 }
953         }
954
955         my %offsets_saved = ();
956         for(@{$progs->{globaldefs}})
957         {
958                 my $type = $_->{type};
959                 my $name = $progs->{getstring}->($_->{s_name});
960                 $name = ''
961                         if $name eq 'IMMEDIATE' or $name =~ /^\./;
962                 $_->{debugname} = $name
963                         if $name ne '';
964                 if($type->{save})
965                 {
966                         for my $i(0..(typesize($_->{type}{type})-1))
967                         {
968                                 $globalflags[$_->{ofs}] |= GLOBALFLAG_S;
969                         }
970                 }
971                 if($name ne '')
972                 {
973                         for my $i(0..(typesize($_->{type}{type})-1))
974                         {
975                                 $globalflags[$_->{ofs}] |= GLOBALFLAG_N;
976                         }
977                 }
978         }
979         my %offsets_initialized = ();
980         for(0..(@{$progs->{globals}}-1))
981         {
982                 if($progs->{globals}[$_]{v}{int})
983                 {
984                         $globalflags[$_] |= GLOBALFLAG_I;
985                 }
986         }
987
988         my @globaltypes = (undef) x @{$progs->{globals}};
989
990         my %istemp = ();
991         for(0..(@{$progs->{globals}}-1))
992         {
993                 next
994                         if $_ < @{(DEFAULTGLOBALS)};
995                 if(($globalflags[$_] & (GLOBALFLAG_R | GLOBALFLAG_W)) == 0)
996                 {
997                         $globaltypes[$_] = "unused";
998                 }
999                 elsif(($globalflags[$_] & (GLOBALFLAG_R | GLOBALFLAG_W)) == GLOBALFLAG_R)
1000                 {
1001                         # so it is ro
1002                         if(($globalflags[$_] & GLOBALFLAG_N) == GLOBALFLAG_N)
1003                         {
1004                                 $globaltypes[$_] = "read_only";
1005                         }
1006                         elsif(($globalflags[$_] & GLOBALFLAG_S) == 0)
1007                         {
1008                                 $globaltypes[$_] = "const";
1009                         }
1010                         else
1011                         {
1012                                 $globaltypes[$_] = "read_only";
1013                         }
1014                 }
1015                 elsif(($globalflags[$_] & (GLOBALFLAG_R | GLOBALFLAG_W)) == GLOBALFLAG_W)
1016                 {
1017                         $globaltypes[$_] = "write_only";
1018                 }
1019                 else
1020                 {
1021                         # now we know it is rw
1022                         if(($globalflags[$_] & GLOBALFLAG_N) == GLOBALFLAG_N)
1023                         {
1024                                 $globaltypes[$_] = "global";
1025                         }
1026                         elsif(($globalflags[$_] & (GLOBALFLAG_S | GLOBALFLAG_I)) == 0)
1027                         {
1028                                 if($globalflags[$_] & GLOBALFLAG_P)
1029                                 {
1030                                         $globaltypes[$_] = "OFS_PARM";
1031                                 }
1032                                 elsif($globalflags[$_] & GLOBALFLAG_Q)
1033                                 {
1034                                         $globaltypes[$_] = "uniquetemp";
1035                                         $istemp{$_} = 0;
1036                                 }
1037                                 else
1038                                 {
1039                                         $globaltypes[$_] = "temp";
1040                                         $istemp{$_} = 1;
1041                                 }
1042                         }
1043                         elsif(($globalflags[$_] & (GLOBALFLAG_S | GLOBALFLAG_I)) == GLOBALFLAG_I)
1044                         {
1045                                 $globaltypes[$_] = "not_saved";
1046                         }
1047                         else
1048                         {
1049                                 $globaltypes[$_] = "global";
1050                         }
1051                 }
1052         }
1053         $progs->{temps} = \%istemp;
1054
1055         # globaldefs
1056         my @globaldefs = (undef) x @{$progs->{globaldefs}};
1057         for(@{$progs->{globaldefs}})
1058         {
1059                 $globaldefs[$_->{ofs}] //= $_
1060                         if defined $_->{debugname};
1061         }
1062         for(@{$progs->{globaldefs}})
1063         {
1064                 $globaldefs[$_->{ofs}] //= $_;
1065         }
1066         for(0..(@{$progs->{globals}}-1))
1067         {
1068                 $globaldefs[$_] //= {
1069                         ofs => $_,
1070                         s_name => undef,
1071                         debugname => undef
1072                 };
1073         }
1074         for(0..(@{(DEFAULTGLOBALS)}-1))
1075         {
1076                 $globaldefs[$_] = { ofs => $_, s_name => undef, debugname => DEFAULTGLOBALS->[$_], type => undef };
1077                 $globaltypes[$_] = 'defglobal';
1078         }
1079         my %globaldefs_namecount = ();
1080         for(@globaldefs)
1081         {
1082                 $_->{globaltype} = $globaltypes[$_->{ofs}];
1083                 if(defined $_->{debugname})
1084                 {
1085                         # already has debugname
1086                 }
1087                 elsif($_->{globaltype} eq 'const')
1088                 {
1089                         $_->{debugname} = get_constant($progs, $progs->{globals}[$_->{ofs}]{v});
1090                 }
1091                 else
1092                 {
1093                         $_->{debugname} = "$_->{globaltype}_$_->{ofs}";
1094                 }
1095                 ++$globaldefs_namecount{$_->{debugname}};
1096         }
1097         for(@globaldefs)
1098         {
1099                 next
1100                         if $globaldefs_namecount{$_->{debugname}} <= 1 && !$ENV{FORCE_OFFSETS};
1101                 #print "Not unique: $_->{debugname} at $_->{ofs}\n";
1102                 $_->{debugname} .= "\@$_->{ofs}";
1103         }
1104         $progs->{globaldef_byoffset} = sub
1105         {
1106                 my ($ofs) = @_;
1107                 my $def = $globaldefs[$ofs];
1108                 return $def;
1109         };
1110 }
1111
1112 sub parse_progs($)
1113 {
1114         my ($fh) = @_;
1115
1116         my %p = ();
1117
1118         print STDERR "Parsing header...\n";
1119         $p{header} = parse_section $fh, DPROGRAMS_T, 0, undef, 1;
1120         
1121         print STDERR "Parsing strings...\n";
1122         $p{strings} = get_section $fh, $p{header}{ofs_strings}, $p{header}{numstrings};
1123         $p{getstring} = sub
1124         {
1125                 my ($startpos) = @_;
1126                 my $endpos = index $p{strings}, "\0", $startpos;
1127                 return substr $p{strings}, $startpos, $endpos - $startpos;
1128         };
1129
1130         print STDERR "Parsing globals...\n";
1131         $p{globals} = [parse_section $fh, DGLOBAL_T, $p{header}{ofs_globals}, undef, $p{header}{numglobals}];
1132
1133         print STDERR "Parsing globaldefs...\n";
1134         $p{globaldefs} = [parse_section $fh, DDEF_T, $p{header}{ofs_globaldefs}, undef, $p{header}{numglobaldefs}];
1135
1136         print STDERR "Range checking globaldefs...\n";
1137         for(0 .. (@{$p{globaldefs}}-1))
1138         {
1139                 my $g = $p{globaldefs}[$_];
1140                 die "Out of range name in globaldef $_"
1141                         if $g->{s_name} < 0 || $g->{s_name} >= length $p{strings};
1142                 my $name = $p{getstring}->($g->{s_name});
1143                 die "Out of range ofs in globaldef $_ (name: \"$name\")"
1144                         if $g->{ofs} >= $p{globals};
1145         }
1146
1147         print STDERR "Parsing fielddefs...\n";
1148         $p{fielddefs} = [parse_section $fh, DDEF_T, $p{header}{ofs_fielddefs}, undef, $p{header}{numfielddefs}];
1149
1150         print STDERR "Range checking fielddefs...\n";
1151         for(0 .. (@{$p{fielddefs}}-1))
1152         {
1153                 my $g = $p{fielddefs}[$_];
1154                 die "Out of range name in fielddef $_"
1155                         if $g->{s_name} < 0 || $g->{s_name} >= length $p{strings};
1156                 my $name = $p{getstring}->($g->{s_name});
1157                 die "Out of range ofs in globaldef $_ (name: \"$name\")"
1158                         if $g->{ofs} >= $p{header}{entityfields};
1159         }
1160
1161         print STDERR "Parsing statements...\n";
1162         $p{statements} = [parse_section $fh, DSTATEMENT_T, $p{header}{ofs_statements}, undef, $p{header}{numstatements}];
1163
1164         print STDERR "Parsing functions...\n";
1165         $p{functions} = [parse_section $fh, DFUNCTION_T, $p{header}{ofs_functions}, undef, $p{header}{numfunctions}];
1166
1167         print STDERR "Range checking functions...\n";
1168         for(0 .. (@{$p{functions}} - 1))
1169         {
1170                 my $f = $p{functions}[$_];
1171                 die "Out of range name in function $_"
1172                         if $f->{s_name} < 0 || $f->{s_name} >= length $p{strings};
1173                 my $name = $p{getstring}->($f->{s_name});
1174                 die "Out of range file in function $_"
1175                         if $f->{s_file} < 0 || $f->{s_file} >= length $p{strings};
1176                 my $file = $p{getstring}->($f->{s_file});
1177                 die "Out of range first_statement in function $_ (name: \"$name\", file: \"$file\")"
1178                         if $f->{first_statement} >= @{$p{statements}};
1179                 die "Out of range parm_start in function $_ (name: \"$name\", file: \"$file\")"
1180                         if $f->{parm_start} < 0 || $f->{parm_start} >= @{$p{globals}};
1181                 die "Out of range locals in function $_ (name: \"$name\", file: \"$file\")"
1182                         if $f->{locals} < 0 || $f->{parm_start} + $f->{locals} >= @{$p{globals}};
1183                 die "Out of range numparms in function $_ (name: \"$name\", file: \"$file\")"
1184                         if $f->{numparms} < 0 || $f->{numparms} > 8;
1185                 my $totalparms = 0;
1186                 for(0..($f->{numparms}-1))
1187                 {
1188                         die "Out of range parm_size[$_] in function $_ (name: \"$name\", file: \"$file\")"
1189                                 unless { 0 => 1, 1 => 1, 3 => 1 }->{$f->{parm_size}[$_]};
1190                         $totalparms += $f->{parm_size}[$_];
1191                 }
1192                 die "Out of range parms in function $_ (name: \"$name\", file: \"$file\")"
1193                         if $f->{locals} < 0 || $f->{parm_start} + $totalparms >= @{$p{globals}};
1194         }
1195
1196         print STDERR "Range checking statements...\n";
1197         for my $ip(0 .. (@{$p{statements}}-1))
1198         {
1199                 my $s = $p{statements}[$ip];
1200                 my $c = checkop $s->{op};
1201
1202                 for(qw(a b c))
1203                 {
1204                         my $type = $c->{$_};
1205                         next
1206                                 unless defined $type;
1207
1208                         if($type eq 'inglobal' || $type eq 'inglobalfunc')
1209                         {
1210                                 $s->{$_} &= 0xFFFF;
1211                                 die "Out of range global offset in statement $ip - cannot continue"
1212                                         if $s->{$_} >= @{$p{globals}};
1213                         }
1214                         elsif($type eq 'inglobalvec')
1215                         {
1216                                 $s->{$_} &= 0xFFFF;
1217                                 die "Out of range global offset in statement $ip - cannot continue"
1218                                         if $s->{$_} >= @{$p{globals}}-2;
1219                         }
1220                         elsif($type eq 'outglobal')
1221                         {
1222                                 $s->{$_} &= 0xFFFF;
1223                                 die "Out of range global offset in statement $ip - cannot continue"
1224                                         if $s->{$_} >= @{$p{globals}};
1225                         }
1226                         elsif($type eq 'outglobalvec')
1227                         {
1228                                 $s->{$_} &= 0xFFFF;
1229                                 die "Out of range global offset in statement $ip - cannot continue"
1230                                         if $s->{$_} >= @{$p{globals}}-2;
1231                         }
1232                         elsif($type eq 'ipoffset')
1233                         {
1234                                 die "Out of range GOTO/IF/IFNOT in statement $ip - cannot continue"
1235                                         if $ip + $s->{$_} < 0 || $ip + $s->{$_} >= @{$p{statements}};
1236                         }
1237                 }
1238         }
1239
1240         print STDERR "Looking for error()...\n";
1241         $p{error_func} = {};
1242         for(@{$p{globaldefs}})
1243         {
1244                 next
1245                         if $p{getstring}($_->{s_name}) ne 'error';
1246                 my $v = $p{globals}[$_->{ofs}]{v}{int};
1247                 next
1248                         if $v <= 0 || $v >= @{$p{functions}};
1249                 my $first = $p{functions}[$v]{first_statement};
1250                 next
1251                         if $first >= 0;
1252                 print STDERR "Detected error() at offset $_->{ofs} (builtin #@{[-$first]})\n";
1253                 $p{error_func}{$_->{ofs}} = 1;
1254         }
1255
1256         print STDERR "Scanning functions...\n";
1257         for(@{$p{functions}})
1258         {
1259                 my $file = $p{getstring}->($_->{s_file});
1260                 my $name = $p{getstring}->($_->{s_name});
1261                 $name = "$file:$name"
1262                         if length $file;
1263                 $_->{debugname} = $name;
1264
1265                 next
1266                         if $_->{first_statement} < 0;
1267
1268                 my %statements = ();
1269                 my %come_from = ();
1270                 my %go_to = ();
1271                 my %globals_read = ();
1272                 my %globals_written = ();
1273                 my %globals_used = ();
1274
1275                 if($_->{first_statement} >= 0)
1276                 {
1277                         run_nfa \%p, $_->{first_statement}, "", id, nfa_default_state_checker,
1278                                 sub
1279                                 {
1280                                         my ($ip, $state, $s, $c) = @_;
1281                                         ++$statements{$ip};
1282
1283                                         if(my $j = $c->{isjump})
1284                                         {
1285                                                 my $t = $ip + $s->{$j};
1286                                                 $come_from{$t}{$ip} = $c->{isconditional};
1287                                                 $go_to{$ip}{$t} = $c->{isconditional};
1288                                         }
1289
1290                                         for my $o(qw(a b c))
1291                                         {
1292                                                 my $type = $c->{$o}
1293                                                         or next;
1294                                                 my $ofs = $s->{$o};
1295
1296                                                 my $read = sub
1297                                                 {
1298                                                         my ($ofs) = @_;
1299                                                         $globals_read{$ofs}{$ip}{$o} = 1;
1300                                                         $globals_used{$ofs} = 1;
1301                                                 };
1302                                                 my $write = sub
1303                                                 {
1304                                                         my ($ofs) = @_;
1305                                                         $globals_written{$ofs}{$ip}{$o} = 1;
1306                                                         $globals_used{$ofs} = 1;
1307                                                 };
1308
1309                                                 if($type eq 'inglobal' || $type eq 'inglobalfunc')
1310                                                 {
1311                                                         $read->($ofs);
1312                                                 }
1313                                                 elsif($type eq 'inglobalvec')
1314                                                 {
1315                                                         $read->($ofs);
1316                                                         $read->($ofs+1);
1317                                                         $read->($ofs+2);
1318                                                 }
1319                                                 elsif($type eq 'outglobal')
1320                                                 {
1321                                                         $write->($ofs);
1322                                                 }
1323                                                 elsif($type eq 'outglobalvec')
1324                                                 {
1325                                                         $write->($ofs);
1326                                                         $write->($ofs+1);
1327                                                         $write->($ofs+2);
1328                                                 }
1329                                         }
1330
1331                                         return 0;
1332                                 };
1333                 }
1334
1335                 $_->{statements} = \%statements;
1336                 $_->{come_from} = \%come_from;
1337                 $_->{go_to} = \%go_to;
1338                 $_->{globals_read} = \%globals_read;
1339                 $_->{globals_written} = \%globals_written;
1340                 $_->{globals_used} = \%globals_used;
1341
1342                 # using this info, we could now identify basic blocks
1343         }
1344
1345         print STDERR "Detecting constants and temps, and naming...\n";
1346         detect_constants \%p;
1347
1348         # what do we want to do?
1349         my $checkfunc = \&find_uninitialized_locals;
1350         if($ENV{DISASSEMBLE})
1351         {
1352                 $checkfunc = \&disassemble_function;
1353         }
1354         for(sort { $a->{debugname} cmp $b->{debugname} } @{$p{functions}})
1355         {
1356                 $checkfunc->(\%p, $_);
1357         }
1358 }
1359
1360 open my $fh, '<', $ARGV[0];
1361 parse_progs $fh;