]> git.xonotic.org Git - xonotic/xonotic.git/blob - misc/tools/progs-analyzer.pl
more fixes
[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         if($func->{first_statement} < 0) # builtin
364         {
365                 printf INSTRUCTION_FORMAT, '', '', '.BUILTIN';
366                 printf OPERAND_FORMAT, -$func->{first_statement};
367                 print INSTRUCTION_SEPARATOR;
368                 return;
369         }
370
371         my $initializer = sub
372         {
373                 my ($ofs) = @_;
374                 my $g = get_constant($progs, $progs->{globals}[$ofs]{v});
375                 print " = $g"
376                         if defined $g;
377         };
378
379         printf INSTRUCTION_FORMAT, '', '', '.PARM_START';
380         printf OPERAND_FORMAT, "$func->{parm_start}";
381         print INSTRUCTION_SEPARATOR;
382
383         printf INSTRUCTION_FORMAT, '', '', '.LOCALS';
384         printf OPERAND_FORMAT, "$func->{locals}";
385         print INSTRUCTION_SEPARATOR;
386
387         my %override_locals = ();
388         my $p = $func->{parm_start};
389         for(0..($func->{numparms}-1))
390         {
391                 $override_locals{$p} //= "argv_$_";
392                 for my $comp(0..($func->{parm_size}[$_]-1))
393                 {
394                         $override_locals{$p} //= "argv_$_\[$comp]";
395                         ++$p;
396                 }
397                 printf INSTRUCTION_FORMAT, '', '', '.ARG';
398                 printf OPERAND_FORMAT, "argv_$_";
399                 print OPERAND_SEPARATOR;
400                 printf OPERAND_FORMAT, $func->{parm_size}[$_];
401                 print INSTRUCTION_SEPARATOR;
402         }
403         for($func->{parm_start}..($func->{parm_start} + $func->{locals} - 1))
404         {
405                 next
406                         if exists $override_locals{$_};
407                 $override_locals{$_} = "local_$_";
408
409                 printf INSTRUCTION_FORMAT, '', '', '.LOCAL';
410                 printf OPERAND_FORMAT, "local_$_";
411                 $initializer->($_);
412                 print INSTRUCTION_SEPARATOR;
413         }
414
415         my $getname = sub
416         {
417                 my ($ofs) = @_;
418                 return $override_locals{$ofs}
419                         if exists $override_locals{$ofs};
420                 my $def = $progs->{globaldef_byoffset}->($ofs);
421                 return $def->{debugname};
422         };
423
424         my $operand = sub
425         {
426                 my ($ip, $type, $operand) = @_;
427                 if($type eq 'inglobal')
428                 {
429                         my $name = $getname->($operand);
430                         printf OPERAND_FORMAT, "$name";
431                 }
432                 elsif($type eq 'outglobal')
433                 {
434                         my $name = $getname->($operand);
435                         printf OPERAND_FORMAT, "&$name";
436                 }
437                 elsif($type eq 'inglobalvec')
438                 {
439                         my $name = $getname->($operand);
440                         printf OPERAND_FORMAT, "$name\[\]";
441                 }
442                 elsif($type eq 'outglobalvec')
443                 {
444                         my $name = $getname->($operand);
445                         printf OPERAND_FORMAT, "&$name\[\]";
446                 }
447                 elsif($type eq 'inglobalfunc')
448                 {
449                         my $name = $getname->($operand);
450                         printf OPERAND_FORMAT, "$name()";
451                 }
452                 elsif($type eq 'ipoffset')
453                 {
454                         printf OPERAND_FORMAT, "@{[$ip + $operand]}" . sprintf ' ($%+d)', $operand;
455                 }
456                 else
457                 {
458                         die "unknown type: $type";
459                 }
460         };
461
462         my $statements = $func->{statements};
463         my $come_from = $func->{come_from};
464
465         my $ipprev = undef;
466         for my $ip(sort { $a <=> $b } keys %$statements)
467         {
468                 if($ip == $func->{first_statement})
469                 {
470                         printf INSTRUCTION_FORMAT, $ip, '', '.ENTRY';
471                         print INSTRUCTION_SEPARATOR;
472                 }
473                 if(defined $ipprev && $ip != $ipprev + 1)
474                 {
475                         printf INSTRUCTION_FORMAT, $ip, '', '.SKIP';
476                         printf OPERAND_FORMAT, $ip - $ipprev - 1;
477                         print INSTRUCTION_SEPARATOR;
478                 }
479                 if(my $cf = $come_from->{$ip})
480                 {
481                         printf INSTRUCTION_FORMAT, $ip, '', '.XREF';
482                         my $cnt = 0;
483                         for(sort { $a <=> $b } keys %$cf)
484                         {
485                                 print OPERAND_SEPARATOR
486                                         if $cnt++;
487                                 printf OPERAND_FORMAT, ($cf->{$_} ? 'c' : 'j') . $_ . sprintf ' ($%+d)', $_ - $ip;
488                         }
489                         print INSTRUCTION_SEPARATOR;
490                 }
491
492                 my $op = $progs->{statements}[$ip]{op};
493                 my $ipt = $progs->{statements}[$ip];
494                 my $opprop = checkop $op;
495
496                 if($highlight and $highlight->{$ip})
497                 {
498                         for(values %{$highlight->{$ip}})
499                         {
500                                 for(@$_)
501                                 {
502                                         print PRE_MARK_STATEMENT;
503                                         printf INSTRUCTION_FORMAT, '', '<!>', '.WARN';
504                                         printf OPERAND_FORMAT, "$_ (in $func->{debugname})";
505                                         print INSTRUCTION_SEPARATOR;
506                                 }
507                         }
508                 }
509
510                 print PRE_MARK_STATEMENT
511                         if $highlight and $highlight->{$ip};
512
513                 my $showip = $opprop->{isjump};
514                 printf INSTRUCTION_FORMAT, $showip ? $ip : '', $highlight->{$ip} ? '<!>' : '', $op;
515
516                 my $cnt = 0;
517                 for my $o(qw(a b c))
518                 {
519                         next
520                                 if not defined $opprop->{$o};
521                         print OPERAND_SEPARATOR
522                                 if $cnt++;
523                         print PRE_MARK_OPERAND
524                                 if $highlight and $highlight->{$ip} and $highlight->{$ip}{$o};
525                         $operand->($ip, $opprop->{$o}, $ipt->{$o});
526                         print POST_MARK_OPERAND
527                                 if $highlight and $highlight->{$ip} and $highlight->{$ip}{$o};
528                 }
529
530                 print POST_MARK_STATEMENT
531                         if $highlight and $highlight->{$ip};
532
533                 print INSTRUCTION_SEPARATOR;
534         }
535 }
536
537 sub find_uninitialized_locals($$)
538 {
539         my ($progs, $func) = @_;
540
541         return
542                 if $func->{first_statement} < 0; # builtin
543
544         print STDERR "Checking $func->{debugname}...\n";
545
546         my $p = $func->{parm_start};
547         for(0..($func->{numparms}-1))
548         {
549                 $p += $func->{parm_size}[$_];
550         }
551
552         use constant WATCHME_R => 1;
553         use constant WATCHME_W => 2;
554         use constant WATCHME_X => 4;
555         use constant WATCHME_T => 8;
556         my %watchme = map { $_ => WATCHME_X } ($func->{parm_start} .. ($func->{parm_start} + $func->{locals} - 1));
557
558         for(keys %{$progs->{temps}})
559         {
560                 next
561                         if exists $watchme{$_};
562                 if($progs->{temps}{$_})
563                 {
564                         # shared temp
565                         $watchme{$_} = WATCHME_T | WATCHME_X
566                 }
567                 else
568                 {
569                         # unique temp
570                         $watchme{$_} = WATCHME_X
571                 }
572         }
573
574         $watchme{$_} |= WATCHME_R
575                 for keys %{$func->{globals_read}};
576         $watchme{$_} |= WATCHME_W
577                 for keys %{$func->{globals_written}};
578
579         my %write_places = ();
580         for my $ofs(keys %{$func->{globals_written}})
581         {
582                 next
583                         unless exists $watchme{$ofs} and $watchme{$ofs} & WATCHME_X;
584                 for my $ip(keys %{$func->{globals_written}{$ofs}})
585                 {
586                         for my $op(keys %{$func->{globals_written}{$ofs}{$ip}})
587                         {
588                                 push @{$write_places{$ip}{$op}}, $ofs;
589                         }
590                 }
591         }
592
593         for(keys %watchme)
594         {
595                 delete $watchme{$_}
596                         if ($watchme{$_} & (WATCHME_R | WATCHME_W | WATCHME_X)) != (WATCHME_R | WATCHME_W | WATCHME_X);
597         }
598
599         return
600                 if not keys %watchme;
601
602         for(keys %watchme)
603         {
604                 $watchme{$_} = {
605                         flags => $watchme{$_},
606                         valid => [0, undef, undef]
607                 };
608         }
609
610         # mark parameters as initialized
611         for($func->{parm_start} .. ($p-1))
612         {
613                 $watchme{$_}{valid} = [1, undef, undef]
614                         if defined $watchme{$_};
615         }
616
617         my %warned = ();
618         my %ip_seen = ();
619         run_nfa $progs, $func->{first_statement}, \%watchme,
620                 sub {
621                         my ($h) = @_;
622                         return { map { $_ => { %{$h->{$_}} } } keys %$h };
623                 },
624                 sub {
625                         my ($ip, $state) = @_;
626
627                         my $s = $ip_seen{$ip};
628                         if($s)
629                         {
630                                 # if $state is stronger or equal to $s, return 1
631
632                                 for(keys %$state)
633                                 {
634                                         if($state->{$_}{valid}[0] < $s->{$_})
635                                         {
636                                                 # The current state is LESS valid than the previously run one. We NEED to run this.
637                                                 # The saved state can safely become the intersection [citation needed].
638                                                 for(keys %$state)
639                                                 {
640                                                         $s->{$_} = $state->{$_}{valid}[0]
641                                                                 if $state->{$_}{valid}[0] < $s->{$_};
642                                                 }
643                                                 return 0;
644                                         }
645                                 }
646                                 # if we get here, $state is stronger or equal. No need to try it.
647                                 return 1;
648                         }
649                         else
650                         {
651                                 # Never seen this IP yet.
652                                 $ip_seen{$ip} = { map { ($_ => $state->{$_}{valid}[0]); } keys %$state };
653                                 return 0;
654                         }
655                 },
656                 sub {
657                         my ($ip, $state, $s, $c) = @_;
658                         my $op = $s->{op};
659
660                         # QCVM BUG: RETURN always takes vector, there is no float equivalent
661                         my $return_hack = $c->{isreturn} // 0;
662
663                         if($op eq 'STORE_V')
664                         {
665                                 # COMPILER BUG of QCC: params are always copied using STORE_V
666                                 if($s->{b} >= 4 && $s->{b} < 28) # parameter range
667                                 {
668                                         $return_hack = 1;
669                                 }
670                         }
671
672                         for(qw(a b c))
673                         {
674                                 my $type = $c->{$_};
675                                 next
676                                         unless defined $type;
677
678                                 my $ofs = $s->{$_};
679
680                                 my $read = sub
681                                 {
682                                         my ($ofs) = @_;
683                                         ++$return_hack
684                                                 if $return_hack;
685                                         return
686                                                 if not exists $state->{$ofs};
687                                         my $valid = $state->{$ofs}{valid};
688                                         if($valid->[0] == 0)
689                                         {
690                                                 # COMPILER BUG of FTEQCC: AND and OR may take uninitialized as second argument (logicops)
691                                                 if($return_hack <= 2 and ($op ne 'OR' && $op ne 'AND' || $_ ne 'b'))
692                                                 {
693                                                         push @{$warned{$ip}{$_}}, "Use of uninitialized value";
694                                                 }
695                                         }
696                                         elsif($valid->[0] < 0)
697                                         {
698                                                 # COMPILER BUG of FTEQCC: AND and OR may take uninitialized as second argument (logicops)
699                                                 if($return_hack <= 2 and ($op ne 'OR' && $op ne 'AND' || $_ ne 'b'))
700                                                 {
701                                                         push @{$warned{$ip}{$_}}, "Use of temporary across CALL";
702                                                 }
703                                         }
704                                         else
705                                         {
706                                                 # it's VALID
707                                                 if(defined $valid->[1])
708                                                 {
709                                                         delete $write_places{$valid->[1]}{$valid->[2]};
710                                                 }
711                                         }
712                                 };
713                                 my $write = sub
714                                 {
715                                         my ($ofs) = @_;
716                                         $state->{$ofs}{valid} = [1, $ip, $_]
717                                                 if exists $state->{$ofs};
718                                 };
719
720                                 if($type eq 'inglobal' || $type eq 'inglobalfunc')
721                                 {
722                                         $read->($ofs);
723                                 }
724                                 elsif($type eq 'inglobalvec')
725                                 {
726                                         $read->($ofs);
727                                         $read->($ofs+1);
728                                         $read->($ofs+2);
729                                 }
730                                 elsif($type eq 'outglobal')
731                                 {
732                                         $write->($ofs);
733                                 }
734                                 elsif($type eq 'outglobalvec')
735                                 {
736                                         $write->($ofs);
737                                         $write->($ofs+1);
738                                         $write->($ofs+2);
739                                 }
740                         }
741                         if($c->{iscall})
742                         {
743                                 # builtin calls may clobber stuff
744                                 my $func = $s->{a};
745                                 my $funcid = $progs->{globals}[$func]{v}{int};
746                                 my $funcobj = $progs->{functions}[$funcid];
747                                 if(!$funcobj || $funcobj->{first_statement} >= 0)
748                                 {
749                                         # invalidate temps
750                                         for(values %$state)
751                                         {
752                                                 if($_->{flags} & WATCHME_T)
753                                                 {
754                                                         $_->{valid} = [-1, undef, undef];
755                                                 }
756                                         }
757                                 }
758                         }
759
760                         return 0;
761                 };
762
763         for my $ip(keys %write_places)
764         {
765                 for my $operand(keys %{$write_places{$ip}})
766                 {
767                         # TODO verify it
768                         my %left = map { $_ => 1 } @{$write_places{$ip}{$operand}};
769                         my $isread = 0;
770
771                         my %writeplace_seen = ();
772                         run_nfa $progs, $ip+1, \%left,
773                                 sub
774                                 {
775                                         return { %{$_[0]} };
776                                 },
777                                 sub
778                                 {
779                                         my ($ip, $state) = @_;
780                                         return $writeplace_seen{"$ip " . join " ", sort keys %$state}++;
781                                 },
782                                 sub
783                                 {
784                                         my ($ip, $state, $s, $c) = @_;
785                                         for(qw(a b c))
786                                         {
787                                                 my $type = $c->{$_};
788                                                 next
789                                                         unless defined $type;
790
791                                                 my $ofs = $s->{$_};
792                                                 if($type eq 'inglobal' || $type eq 'inglobalfunc')
793                                                 {
794                                                         if($state->{$ofs})
795                                                         {
796                                                                 $isread = 1;
797                                                                 return -1; # exit TOTALLY
798                                                         }
799                                                 }
800                                                 elsif($type eq 'inglobalvec')
801                                                 {
802                                                         if($state->{$ofs} || $state->{$ofs+1} || $state->{$ofs+2})
803                                                         {
804                                                                 $isread = 1;
805                                                                 return -1; # exit TOTALLY
806                                                         }
807                                                 }
808                                                 elsif($type eq 'outglobal')
809                                                 {
810                                                         delete $state->{$ofs};
811                                                         return 1
812                                                                 if !%$state;
813                                                 }
814                                                 elsif($type eq 'outglobalvec')
815                                                 {
816                                                         delete $state->{$ofs};
817                                                         delete $state->{$ofs+1};
818                                                         delete $state->{$ofs+2};
819                                                         return 1
820                                                                 if !%$state;
821                                                 }
822                                         }
823                                         return 0;
824                                 };
825
826                         if(!$isread)
827                         {
828                                 push @{$warned{$ip}{$operand}}, "Value is never used";
829                         }
830                 }
831         }
832         
833         disassemble_function($progs, $func, \%warned)
834                 if keys %warned;
835 }
836
837 use constant DEFAULTGLOBALS => [
838         "OFS_NULL",
839         "OFS_RETURN",
840         "OFS_RETURN[1]",
841         "OFS_RETURN[2]",
842         "OFS_PARM0",
843         "OFS_PARM0[1]",
844         "OFS_PARM0[2]",
845         "OFS_PARM1",
846         "OFS_PARM1[1]",
847         "OFS_PARM1[2]",
848         "OFS_PARM2",
849         "OFS_PARM2[1]",
850         "OFS_PARM2[2]",
851         "OFS_PARM3",
852         "OFS_PARM3[1]",
853         "OFS_PARM3[2]",
854         "OFS_PARM4",
855         "OFS_PARM4[1]",
856         "OFS_PARM4[2]",
857         "OFS_PARM5",
858         "OFS_PARM5[1]",
859         "OFS_PARM5[2]",
860         "OFS_PARM6",
861         "OFS_PARM6[1]",
862         "OFS_PARM6[2]",
863         "OFS_PARM7",
864         "OFS_PARM7[1]",
865         "OFS_PARM7[2]"
866 ];
867
868 sub defaultglobal($)
869 {
870         my ($ofs) = @_;
871         if($ofs < @{(DEFAULTGLOBALS)})
872         {
873                 return { ofs => $ofs, s_name => undef, debugname => DEFAULTGLOBALS->[$ofs], type => undef };
874         }
875         return { ofs => $ofs, s_name => undef, debugname => "<undefined>\@$ofs", type => undef };
876 }
877
878 sub detect_constants($)
879 {
880         my ($progs) = @_;
881         use constant GLOBALFLAG_R => 1; # read
882         use constant GLOBALFLAG_W => 2; # written
883         use constant GLOBALFLAG_S => 4; # saved
884         use constant GLOBALFLAG_I => 8; # initialized
885         use constant GLOBALFLAG_N => 16; # named
886         use constant GLOBALFLAG_Q => 32; # unique to function
887         use constant GLOBALFLAG_U => 64; # unused
888         use constant GLOBALFLAG_P => 128; # possibly parameter passing
889         my @globalflags = (GLOBALFLAG_Q | GLOBALFLAG_U) x @{$progs->{globals}};
890
891         for(@{$progs->{functions}})
892         {
893                 for(keys %{$_->{globals_used}})
894                 {
895                         if($globalflags[$_] & GLOBALFLAG_U)
896                         {
897                                 $globalflags[$_] &= ~GLOBALFLAG_U;
898                         }
899                         elsif($globalflags[$_] & GLOBALFLAG_Q)
900                         {
901                                 $globalflags[$_] &= ~GLOBALFLAG_Q;
902                         }
903                 }
904                 $globalflags[$_] |= GLOBALFLAG_R
905                         for keys %{$_->{globals_read}};
906                 $globalflags[$_] |= GLOBALFLAG_W
907                         for keys %{$_->{globals_written}};
908                 next
909                         if $_->{first_statement} < 0;
910                 for my $ip($_->{first_statement} .. (@{$progs->{statements}}-1))
911                 {
912                         my $s = $progs->{statements}[$ip];
913                         if($s->{op} eq 'STORE_V')
914                         {
915                                 $globalflags[$s->{a}] |= GLOBALFLAG_P
916                                         if $s->{b} >= $_->{parm_start} and $s->{b} < $_->{parm_start} + $_->{locals};
917                                 $globalflags[$s->{a}+1] |= GLOBALFLAG_P
918                                         if $s->{b}+1 >= $_->{parm_start} and $s->{b}+1 < $_->{parm_start} + $_->{locals};
919                                 $globalflags[$s->{a}+2] |= GLOBALFLAG_P
920                                         if $s->{b}+2 >= $_->{parm_start} and $s->{b}+2 < $_->{parm_start} + $_->{locals};
921                         }
922                         elsif($s->{op} =~ /^STORE_/)
923                         {
924                                 $globalflags[$s->{a}] |= GLOBALFLAG_P
925                                         if $s->{b} >= $_->{parm_start} and $s->{b} < $_->{parm_start} + $_->{locals};
926                         }
927                         else
928                         {
929                                 last;
930                         }
931                 }
932         }
933
934         # parameter passing globals are only ever used in STORE_ instructions
935         for my $s(@{$progs->{statements}})
936         {
937                 next
938                         if $s->{op} =~ /^STORE_/;
939
940                 my $c = checkop $s->{op};
941
942                 for(qw(a b c))
943                 {
944                         my $type = $c->{$_};
945                         next
946                                 unless defined $type;
947
948                         my $ofs = $s->{$_};
949                         if($type eq 'inglobal' || $type eq 'inglobalfunc' || $type eq 'outglobal')
950                         {
951                                 $globalflags[$ofs] &= ~GLOBALFLAG_P;
952                         }
953                         if($type eq 'inglobalvec' || $type eq 'outglobalvec')
954                         {
955                                 $globalflags[$ofs] &= ~GLOBALFLAG_P;
956                                 $globalflags[$ofs+1] &= ~GLOBALFLAG_P;
957                                 $globalflags[$ofs+2] &= ~GLOBALFLAG_P;
958                         }
959                 }
960         }
961
962         my %offsets_saved = ();
963         for(@{$progs->{globaldefs}})
964         {
965                 my $type = $_->{type};
966                 my $name = $progs->{getstring}->($_->{s_name});
967                 $name = ''
968                         if $name eq 'IMMEDIATE' or $name =~ /^\./;
969                 $_->{debugname} = $name
970                         if $name ne '';
971                 if($type->{save})
972                 {
973                         for my $i(0..(typesize($_->{type}{type})-1))
974                         {
975                                 $globalflags[$_->{ofs}] |= GLOBALFLAG_S;
976                         }
977                 }
978                 if($name ne '')
979                 {
980                         for my $i(0..(typesize($_->{type}{type})-1))
981                         {
982                                 $globalflags[$_->{ofs}] |= GLOBALFLAG_N;
983                         }
984                 }
985         }
986         my %offsets_initialized = ();
987         for(0..(@{$progs->{globals}}-1))
988         {
989                 if($progs->{globals}[$_]{v}{int})
990                 {
991                         $globalflags[$_] |= GLOBALFLAG_I;
992                 }
993         }
994
995         my @globaltypes = (undef) x @{$progs->{globals}};
996
997         my %istemp = ();
998         for(0..(@{$progs->{globals}}-1))
999         {
1000                 next
1001                         if $_ < @{(DEFAULTGLOBALS)};
1002                 if(($globalflags[$_] & (GLOBALFLAG_R | GLOBALFLAG_W)) == 0)
1003                 {
1004                         $globaltypes[$_] = "unused";
1005                 }
1006                 elsif(($globalflags[$_] & (GLOBALFLAG_R | GLOBALFLAG_W)) == GLOBALFLAG_R)
1007                 {
1008                         # so it is ro
1009                         if(($globalflags[$_] & GLOBALFLAG_N) == GLOBALFLAG_N)
1010                         {
1011                                 $globaltypes[$_] = "read_only";
1012                         }
1013                         elsif(($globalflags[$_] & GLOBALFLAG_S) == 0)
1014                         {
1015                                 $globaltypes[$_] = "const";
1016                         }
1017                         else
1018                         {
1019                                 $globaltypes[$_] = "read_only";
1020                         }
1021                 }
1022                 elsif(($globalflags[$_] & (GLOBALFLAG_R | GLOBALFLAG_W)) == GLOBALFLAG_W)
1023                 {
1024                         $globaltypes[$_] = "write_only";
1025                 }
1026                 else
1027                 {
1028                         # now we know it is rw
1029                         if(($globalflags[$_] & GLOBALFLAG_N) == GLOBALFLAG_N)
1030                         {
1031                                 $globaltypes[$_] = "global";
1032                         }
1033                         elsif(($globalflags[$_] & (GLOBALFLAG_S | GLOBALFLAG_I)) == 0)
1034                         {
1035                                 if($globalflags[$_] & GLOBALFLAG_P)
1036                                 {
1037                                         $globaltypes[$_] = "OFS_PARM";
1038                                 }
1039                                 elsif($globalflags[$_] & GLOBALFLAG_Q)
1040                                 {
1041                                         $globaltypes[$_] = "uniquetemp";
1042                                         $istemp{$_} = 0;
1043                                 }
1044                                 else
1045                                 {
1046                                         $globaltypes[$_] = "temp";
1047                                         $istemp{$_} = 1;
1048                                 }
1049                         }
1050                         elsif(($globalflags[$_] & (GLOBALFLAG_S | GLOBALFLAG_I)) == GLOBALFLAG_I)
1051                         {
1052                                 $globaltypes[$_] = "not_saved";
1053                         }
1054                         else
1055                         {
1056                                 $globaltypes[$_] = "global";
1057                         }
1058                 }
1059         }
1060         $progs->{temps} = \%istemp;
1061
1062         # globaldefs
1063         my @globaldefs = (undef) x @{$progs->{globals}};
1064         for(@{$progs->{globaldefs}})
1065         {
1066                 $globaldefs[$_->{ofs}] //= $_
1067                         if defined $_->{debugname};
1068         }
1069         for(@{$progs->{globaldefs}})
1070         {
1071                 $globaldefs[$_->{ofs}] //= $_;
1072         }
1073         for(0..(@{$progs->{globals}}-1))
1074         {
1075                 $globaldefs[$_] //= {
1076                         ofs => $_,
1077                         s_name => undef,
1078                         debugname => undef
1079                 };
1080         }
1081         for(0..(@{(DEFAULTGLOBALS)}-1))
1082         {
1083                 $globaldefs[$_] = { ofs => $_, s_name => undef, debugname => DEFAULTGLOBALS->[$_], type => undef };
1084                 $globaltypes[$_] = 'defglobal';
1085         }
1086         my %globaldefs_namecount = ();
1087         for(@globaldefs)
1088         {
1089                 $_->{globaltype} = $globaltypes[$_->{ofs}];
1090                 if(defined $_->{debugname})
1091                 {
1092                         # already has debugname
1093                 }
1094                 elsif($_->{globaltype} eq 'const')
1095                 {
1096                         $_->{debugname} = get_constant($progs, $progs->{globals}[$_->{ofs}]{v});
1097                 }
1098                 else
1099                 {
1100                         $_->{debugname} = "$_->{globaltype}_$_->{ofs}";
1101                 }
1102                 ++$globaldefs_namecount{$_->{debugname}};
1103         }
1104         for(@globaldefs)
1105         {
1106                 next
1107                         if $globaldefs_namecount{$_->{debugname}} <= 1 && !$ENV{FORCE_OFFSETS};
1108                 #print "Not unique: $_->{debugname} at $_->{ofs}\n";
1109                 $_->{debugname} .= "\@$_->{ofs}";
1110         }
1111         $progs->{globaldef_byoffset} = sub
1112         {
1113                 my ($ofs) = @_;
1114                 my $def = $globaldefs[$ofs];
1115                 return $def;
1116         };
1117 }
1118
1119 sub parse_progs($)
1120 {
1121         my ($fh) = @_;
1122
1123         my %p = ();
1124
1125         print STDERR "Parsing header...\n";
1126         $p{header} = parse_section $fh, DPROGRAMS_T, 0, undef, 1;
1127         
1128         print STDERR "Parsing strings...\n";
1129         $p{strings} = get_section $fh, $p{header}{ofs_strings}, $p{header}{numstrings};
1130         $p{getstring} = sub
1131         {
1132                 my ($startpos) = @_;
1133                 my $endpos = index $p{strings}, "\0", $startpos;
1134                 return substr $p{strings}, $startpos, $endpos - $startpos;
1135         };
1136
1137         print STDERR "Parsing globals...\n";
1138         $p{globals} = [parse_section $fh, DGLOBAL_T, $p{header}{ofs_globals}, undef, $p{header}{numglobals}];
1139
1140         print STDERR "Parsing globaldefs...\n";
1141         $p{globaldefs} = [parse_section $fh, DDEF_T, $p{header}{ofs_globaldefs}, undef, $p{header}{numglobaldefs}];
1142
1143         print STDERR "Range checking globaldefs...\n";
1144         for(0 .. (@{$p{globaldefs}}-1))
1145         {
1146                 my $g = $p{globaldefs}[$_];
1147                 die "Out of range name in globaldef $_"
1148                         if $g->{s_name} < 0 || $g->{s_name} >= length $p{strings};
1149                 my $name = $p{getstring}->($g->{s_name});
1150                 die "Out of range ofs in globaldef $_ (name: \"$name\")"
1151                         if $g->{ofs} >= $p{globals};
1152         }
1153
1154         print STDERR "Parsing fielddefs...\n";
1155         $p{fielddefs} = [parse_section $fh, DDEF_T, $p{header}{ofs_fielddefs}, undef, $p{header}{numfielddefs}];
1156
1157         print STDERR "Range checking fielddefs...\n";
1158         for(0 .. (@{$p{fielddefs}}-1))
1159         {
1160                 my $g = $p{fielddefs}[$_];
1161                 die "Out of range name in fielddef $_"
1162                         if $g->{s_name} < 0 || $g->{s_name} >= length $p{strings};
1163                 my $name = $p{getstring}->($g->{s_name});
1164                 die "Out of range ofs in globaldef $_ (name: \"$name\")"
1165                         if $g->{ofs} >= $p{header}{entityfields};
1166         }
1167
1168         print STDERR "Parsing statements...\n";
1169         $p{statements} = [parse_section $fh, DSTATEMENT_T, $p{header}{ofs_statements}, undef, $p{header}{numstatements}];
1170
1171         print STDERR "Parsing functions...\n";
1172         $p{functions} = [parse_section $fh, DFUNCTION_T, $p{header}{ofs_functions}, undef, $p{header}{numfunctions}];
1173
1174         print STDERR "Range checking functions...\n";
1175         for(0 .. (@{$p{functions}} - 1))
1176         {
1177                 my $f = $p{functions}[$_];
1178                 die "Out of range name in function $_"
1179                         if $f->{s_name} < 0 || $f->{s_name} >= length $p{strings};
1180                 my $name = $p{getstring}->($f->{s_name});
1181                 die "Out of range file in function $_"
1182                         if $f->{s_file} < 0 || $f->{s_file} >= length $p{strings};
1183                 my $file = $p{getstring}->($f->{s_file});
1184                 die "Out of range first_statement in function $_ (name: \"$name\", file: \"$file\")"
1185                         if $f->{first_statement} >= @{$p{statements}};
1186                 die "Out of range parm_start in function $_ (name: \"$name\", file: \"$file\")"
1187                         if $f->{parm_start} < 0 || $f->{parm_start} >= @{$p{globals}};
1188                 die "Out of range locals in function $_ (name: \"$name\", file: \"$file\")"
1189                         if $f->{locals} < 0 || $f->{parm_start} + $f->{locals} >= @{$p{globals}};
1190                 die "Out of range numparms in function $_ (name: \"$name\", file: \"$file\")"
1191                         if $f->{numparms} < 0 || $f->{numparms} > 8;
1192                 my $totalparms = 0;
1193                 for(0..($f->{numparms}-1))
1194                 {
1195                         die "Out of range parm_size[$_] in function $_ (name: \"$name\", file: \"$file\")"
1196                                 unless { 0 => 1, 1 => 1, 3 => 1 }->{$f->{parm_size}[$_]};
1197                         $totalparms += $f->{parm_size}[$_];
1198                 }
1199                 die "Out of range parms in function $_ (name: \"$name\", file: \"$file\")"
1200                         if $f->{locals} < 0 || $f->{parm_start} + $totalparms >= @{$p{globals}};
1201         }
1202
1203         print STDERR "Range checking statements...\n";
1204         for my $ip(0 .. (@{$p{statements}}-1))
1205         {
1206                 my $s = $p{statements}[$ip];
1207                 my $c = checkop $s->{op};
1208
1209                 for(qw(a b c))
1210                 {
1211                         my $type = $c->{$_};
1212                         next
1213                                 unless defined $type;
1214
1215                         if($type eq 'inglobal' || $type eq 'inglobalfunc')
1216                         {
1217                                 $s->{$_} &= 0xFFFF;
1218                                 die "Out of range global offset in statement $ip - cannot continue"
1219                                         if $s->{$_} >= @{$p{globals}};
1220                         }
1221                         elsif($type eq 'inglobalvec')
1222                         {
1223                                 $s->{$_} &= 0xFFFF;
1224                                 die "Out of range global offset in statement $ip - cannot continue"
1225                                         if $s->{$_} >= @{$p{globals}}-2;
1226                         }
1227                         elsif($type eq 'outglobal')
1228                         {
1229                                 $s->{$_} &= 0xFFFF;
1230                                 die "Out of range global offset in statement $ip - cannot continue"
1231                                         if $s->{$_} >= @{$p{globals}};
1232                         }
1233                         elsif($type eq 'outglobalvec')
1234                         {
1235                                 $s->{$_} &= 0xFFFF;
1236                                 die "Out of range global offset in statement $ip - cannot continue"
1237                                         if $s->{$_} >= @{$p{globals}}-2;
1238                         }
1239                         elsif($type eq 'ipoffset')
1240                         {
1241                                 die "Out of range GOTO/IF/IFNOT in statement $ip - cannot continue"
1242                                         if $ip + $s->{$_} < 0 || $ip + $s->{$_} >= @{$p{statements}};
1243                         }
1244                 }
1245         }
1246
1247         print STDERR "Looking for error()...\n";
1248         $p{error_func} = {};
1249         for(@{$p{globaldefs}})
1250         {
1251                 next
1252                         if $p{getstring}($_->{s_name}) ne 'error';
1253                 my $v = $p{globals}[$_->{ofs}]{v}{int};
1254                 next
1255                         if $v <= 0 || $v >= @{$p{functions}};
1256                 my $first = $p{functions}[$v]{first_statement};
1257                 next
1258                         if $first >= 0;
1259                 print STDERR "Detected error() at offset $_->{ofs} (builtin #@{[-$first]})\n";
1260                 $p{error_func}{$_->{ofs}} = 1;
1261         }
1262
1263         print STDERR "Scanning functions...\n";
1264         for(@{$p{functions}})
1265         {
1266                 my $file = $p{getstring}->($_->{s_file});
1267                 my $name = $p{getstring}->($_->{s_name});
1268                 $name = "$file:$name"
1269                         if length $file;
1270                 $_->{debugname} = $name;
1271
1272                 next
1273                         if $_->{first_statement} < 0;
1274
1275                 my %statements = ();
1276                 my %come_from = ();
1277                 my %go_to = ();
1278                 my %globals_read = ();
1279                 my %globals_written = ();
1280                 my %globals_used = ();
1281
1282                 if($_->{first_statement} >= 0)
1283                 {
1284                         run_nfa \%p, $_->{first_statement}, "", id, nfa_default_state_checker,
1285                                 sub
1286                                 {
1287                                         my ($ip, $state, $s, $c) = @_;
1288                                         ++$statements{$ip};
1289
1290                                         if(my $j = $c->{isjump})
1291                                         {
1292                                                 my $t = $ip + $s->{$j};
1293                                                 $come_from{$t}{$ip} = $c->{isconditional};
1294                                                 $go_to{$ip}{$t} = $c->{isconditional};
1295                                         }
1296
1297                                         for my $o(qw(a b c))
1298                                         {
1299                                                 my $type = $c->{$o}
1300                                                         or next;
1301                                                 my $ofs = $s->{$o};
1302
1303                                                 my $read = sub
1304                                                 {
1305                                                         my ($ofs) = @_;
1306                                                         $globals_read{$ofs}{$ip}{$o} = 1;
1307                                                         $globals_used{$ofs} = 1;
1308                                                 };
1309                                                 my $write = sub
1310                                                 {
1311                                                         my ($ofs) = @_;
1312                                                         $globals_written{$ofs}{$ip}{$o} = 1;
1313                                                         $globals_used{$ofs} = 1;
1314                                                 };
1315
1316                                                 if($type eq 'inglobal' || $type eq 'inglobalfunc')
1317                                                 {
1318                                                         $read->($ofs);
1319                                                 }
1320                                                 elsif($type eq 'inglobalvec')
1321                                                 {
1322                                                         $read->($ofs);
1323                                                         $read->($ofs+1);
1324                                                         $read->($ofs+2);
1325                                                 }
1326                                                 elsif($type eq 'outglobal')
1327                                                 {
1328                                                         $write->($ofs);
1329                                                 }
1330                                                 elsif($type eq 'outglobalvec')
1331                                                 {
1332                                                         $write->($ofs);
1333                                                         $write->($ofs+1);
1334                                                         $write->($ofs+2);
1335                                                 }
1336                                         }
1337
1338                                         return 0;
1339                                 };
1340                 }
1341
1342                 $_->{statements} = \%statements;
1343                 $_->{come_from} = \%come_from;
1344                 $_->{go_to} = \%go_to;
1345                 $_->{globals_read} = \%globals_read;
1346                 $_->{globals_written} = \%globals_written;
1347                 $_->{globals_used} = \%globals_used;
1348
1349                 # using this info, we could now identify basic blocks
1350         }
1351
1352         print STDERR "Detecting constants and temps, and naming...\n";
1353         detect_constants \%p;
1354
1355         if($ENV{DUMP})
1356         {
1357                 use Data::Dumper;
1358                 print Dumper \%p;
1359                 return;
1360         }
1361
1362         # what do we want to do?
1363         my $checkfunc = \&find_uninitialized_locals;
1364         if($ENV{DISASSEMBLE})
1365         {
1366                 $checkfunc = \&disassemble_function;
1367         }
1368         for(sort { $a->{debugname} cmp $b->{debugname} } @{$p{functions}})
1369         {
1370                 $checkfunc->(\%p, $_);
1371         }
1372 }
1373
1374 open my $fh, '<', $ARGV[0];
1375 parse_progs $fh;