]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - plugins/Minify/extlib/minify/min/lib/JSMinPlus.php
31a1a5cb48e550928121af091262bbb5f589d436
[quix0rs-gnu-social.git] / plugins / Minify / extlib / minify / min / lib / JSMinPlus.php
1 <?php
2
3 /**
4  * JSMinPlus version 1.1
5  *
6  * Minifies a javascript file using a javascript parser
7  *
8  * This implements a PHP port of Brendan Eich's Narcissus open source javascript engine (in javascript)
9  * References: http://en.wikipedia.org/wiki/Narcissus_(JavaScript_engine)
10  * Narcissus sourcecode: http://mxr.mozilla.org/mozilla/source/js/narcissus/
11  * JSMinPlus weblog: http://crisp.tweakblogs.net/blog/cat/716
12  *
13  * Tino Zijdel <crisp@tweakers.net>
14  *
15  * Usage: $minified = JSMinPlus::minify($script [, $filename])
16  *
17  * Versionlog (see also changelog.txt):
18  * 12-04-2009 - some small bugfixes and performance improvements
19  * 09-04-2009 - initial open sourced version 1.0
20  *
21  * Latest version of this script: http://files.tweakers.net/jsminplus/jsminplus.zip
22  *
23  */
24
25 /* ***** BEGIN LICENSE BLOCK *****
26  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
27  *
28  * The contents of this file are subject to the Mozilla Public License Version
29  * 1.1 (the "License"); you may not use this file except in compliance with
30  * the License. You may obtain a copy of the License at
31  * http://www.mozilla.org/MPL/
32  *
33  * Software distributed under the License is distributed on an "AS IS" basis,
34  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
35  * for the specific language governing rights and limitations under the
36  * License.
37  *
38  * The Original Code is the Narcissus JavaScript engine.
39  *
40  * The Initial Developer of the Original Code is
41  * Brendan Eich <brendan@mozilla.org>.
42  * Portions created by the Initial Developer are Copyright (C) 2004
43  * the Initial Developer. All Rights Reserved.
44  *
45  * Contributor(s): Tino Zijdel <crisp@tweakers.net>
46  * PHP port, modifications and minifier routine are (C) 2009
47  *
48  * Alternatively, the contents of this file may be used under the terms of
49  * either the GNU General Public License Version 2 or later (the "GPL"), or
50  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
51  * in which case the provisions of the GPL or the LGPL are applicable instead
52  * of those above. If you wish to allow use of your version of this file only
53  * under the terms of either the GPL or the LGPL, and not to allow others to
54  * use your version of this file under the terms of the MPL, indicate your
55  * decision by deleting the provisions above and replace them with the notice
56  * and other provisions required by the GPL or the LGPL. If you do not delete
57  * the provisions above, a recipient may use your version of this file under
58  * the terms of any one of the MPL, the GPL or the LGPL.
59  *
60  * ***** END LICENSE BLOCK ***** */
61
62 define('TOKEN_END', 1);
63 define('TOKEN_NUMBER', 2);
64 define('TOKEN_IDENTIFIER', 3);
65 define('TOKEN_STRING', 4);
66 define('TOKEN_REGEXP', 5);
67 define('TOKEN_NEWLINE', 6);
68 define('TOKEN_CONDCOMMENT_MULTILINE', 7);
69
70 define('JS_SCRIPT', 100);
71 define('JS_BLOCK', 101);
72 define('JS_LABEL', 102);
73 define('JS_FOR_IN', 103);
74 define('JS_CALL', 104);
75 define('JS_NEW_WITH_ARGS', 105);
76 define('JS_INDEX', 106);
77 define('JS_ARRAY_INIT', 107);
78 define('JS_OBJECT_INIT', 108);
79 define('JS_PROPERTY_INIT', 109);
80 define('JS_GETTER', 110);
81 define('JS_SETTER', 111);
82 define('JS_GROUP', 112);
83 define('JS_LIST', 113);
84
85 define('DECLARED_FORM', 0);
86 define('EXPRESSED_FORM', 1);
87 define('STATEMENT_FORM', 2);
88
89 class JSMinPlus
90 {
91         private $parser;
92         private $reserved = array(
93                 'break', 'case', 'catch', 'continue', 'default', 'delete', 'do',
94                 'else', 'finally', 'for', 'function', 'if', 'in', 'instanceof',
95                 'new', 'return', 'switch', 'this', 'throw', 'try', 'typeof', 'var',
96                 'void', 'while', 'with',
97                 // Words reserved for future use
98                 'abstract', 'boolean', 'byte', 'char', 'class', 'const', 'debugger',
99                 'double', 'enum', 'export', 'extends', 'final', 'float', 'goto',
100                 'implements', 'import', 'int', 'interface', 'long', 'native',
101                 'package', 'private', 'protected', 'public', 'short', 'static',
102                 'super', 'synchronized', 'throws', 'transient', 'volatile',
103                 // These are not reserved, but should be taken into account
104                 // in isValidIdentifier (See jslint source code)
105                 'arguments', 'eval', 'true', 'false', 'Infinity', 'NaN', 'null', 'undefined'
106         );
107
108         private function __construct()
109         {
110                 $this->parser = new JSParser();
111         }
112
113         public static function minify($js, $filename='')
114         {
115                 static $instance;
116
117                 // this is a singleton
118                 if(!$instance)
119                         $instance = new JSMinPlus();
120
121                 return $instance->min($js, $filename);
122         }
123
124         private function min($js, $filename)
125         {
126                 try
127                 {
128                         $n = $this->parser->parse($js, $filename, 1);
129                         return $this->parseTree($n);
130                 }
131                 catch(Exception $e)
132                 {
133                         echo $e->getMessage() . "\n";
134                 }
135
136                 return false;
137         }
138
139         private function parseTree($n, $noBlockGrouping = false)
140         {
141                 $s = '';
142
143                 switch ($n->type)
144                 {
145                         case KEYWORD_FUNCTION:
146                                 $s .= 'function' . ($n->name ? ' ' . $n->name : '') . '(';
147                                 $params = $n->params;
148                                 for ($i = 0, $j = count($params); $i < $j; $i++)
149                                         $s .= ($i ? ',' : '') . $params[$i];
150                                 $s .= '){' . $this->parseTree($n->body, true) . '}';
151                         break;
152
153                         case JS_SCRIPT:
154                                 // we do nothing with funDecls or varDecls
155                                 $noBlockGrouping = true;
156                         // fall through
157                         case JS_BLOCK:
158                                 $childs = $n->treeNodes;
159                                 for ($c = 0, $i = 0, $j = count($childs); $i < $j; $i++)
160                                 {
161                                         $t = $this->parseTree($childs[$i]);
162                                         if (strlen($t))
163                                         {
164                                                 if ($c)
165                                                 {
166                                                         if ($childs[$i]->type == KEYWORD_FUNCTION && $childs[$i]->functionForm == DECLARED_FORM)
167                                                                 $s .= "\n"; // put declared functions on a new line
168                                                         else
169                                                                 $s .= ';';
170                                                 }
171
172                                                 $s .= $t;
173
174                                                 $c++;
175                                         }
176                                 }
177
178                                 if ($c > 1 && !$noBlockGrouping)
179                                 {
180                                         $s = '{' . $s . '}';
181                                 }
182                         break;
183
184                         case KEYWORD_IF:
185                                 $s = 'if(' . $this->parseTree($n->condition) . ')';
186                                 $thenPart = $this->parseTree($n->thenPart);
187                                 $elsePart = $n->elsePart ? $this->parseTree($n->elsePart) : null;
188
189                                 // quite a rancid hack to see if we should enclose the thenpart in brackets
190                                 if ($thenPart[0] != '{')
191                                 {
192                                         if (strpos($thenPart, 'if(') !== false)
193                                                 $thenPart = '{' . $thenPart . '}';
194                                         elseif ($elsePart)
195                                                 $thenPart .= ';';
196                                 }
197
198                                 $s .= $thenPart;
199
200                                 if ($elsePart)
201                                 {
202                                         $s .= 'else';
203
204                                         if ($elsePart[0] != '{')
205                                                 $s .= ' ';
206
207                                         $s .= $elsePart;
208                                 }
209                         break;
210
211                         case KEYWORD_SWITCH:
212                                 $s = 'switch(' . $this->parseTree($n->discriminant) . '){';
213                                 $cases = $n->cases;
214                                 for ($i = 0, $j = count($cases); $i < $j; $i++)
215                                 {
216                                         $case = $cases[$i];
217                                         if ($case->type == KEYWORD_CASE)
218                                                 $s .= 'case' . ($case->caseLabel->type != TOKEN_STRING ? ' ' : '') . $this->parseTree($case->caseLabel) . ':';
219                                         else
220                                                 $s .= 'default:';
221
222                                         $statement = $this->parseTree($case->statements);
223                                         if ($statement)
224                                                 $s .= $statement . ';';
225                                 }
226                                 $s = rtrim($s, ';') . '}';
227                         break;
228
229                         case KEYWORD_FOR:
230                                 $s = 'for(' . ($n->setup ? $this->parseTree($n->setup) : '')
231                                         . ';' . ($n->condition ? $this->parseTree($n->condition) : '')
232                                         . ';' . ($n->update ? $this->parseTree($n->update) : '') . ')'
233                                         . $this->parseTree($n->body);
234                         break;
235
236                         case KEYWORD_WHILE:
237                                 $s = 'while(' . $this->parseTree($n->condition) . ')' . $this->parseTree($n->body);
238                         break;
239
240                         case JS_FOR_IN:
241                                 $s = 'for(' . ($n->varDecl ? $this->parseTree($n->varDecl) : $this->parseTree($n->iterator)) . ' in ' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
242                         break;
243
244                         case KEYWORD_DO:
245                                 $s = 'do{' . $this->parseTree($n->body, true) . '}while(' . $this->parseTree($n->condition) . ')';
246                         break;
247
248                         case KEYWORD_BREAK:
249                         case KEYWORD_CONTINUE:
250                                 $s = $n->value . ($n->label ? ' ' . $n->label : '');
251                         break;
252
253                         case KEYWORD_TRY:
254                                 $s = 'try{' . $this->parseTree($n->tryBlock, true) . '}';
255                                 $catchClauses = $n->catchClauses;
256                                 for ($i = 0, $j = count($catchClauses); $i < $j; $i++)
257                                 {
258                                         $t = $catchClauses[$i];
259                                         $s .= 'catch(' . $t->varName . ($t->guard ? ' if ' . $this->parseTree($t->guard) : '') . '){' . $this->parseTree($t->block, true) . '}';
260                                 }
261                                 if ($n->finallyBlock)
262                                         $s .= 'finally{' . $this->parseTree($n->finallyBlock, true) . '}';
263                         break;
264
265                         case KEYWORD_THROW:
266                                 $s = 'throw ' . $this->parseTree($n->exception);
267                         break;
268
269                         case KEYWORD_RETURN:
270                                 $s = 'return' . ($n->value ? ' ' . $this->parseTree($n->value) : '');
271                         break;
272
273                         case KEYWORD_WITH:
274                                 $s = 'with(' . $this->parseTree($n->object) . ')' . $this->parseTree($n->body);
275                         break;
276
277                         case KEYWORD_VAR:
278                         case KEYWORD_CONST:
279                                 $s = $n->value . ' ';
280                                 $childs = $n->treeNodes;
281                                 for ($i = 0, $j = count($childs); $i < $j; $i++)
282                                 {
283                                         $t = $childs[$i];
284                                         $s .= ($i ? ',' : '') . $t->name;
285                                         $u = $t->initializer;
286                                         if ($u)
287                                                 $s .= '=' . $this->parseTree($u);
288                                 }
289                         break;
290
291                         case KEYWORD_DEBUGGER:
292                                 throw new Exception('NOT IMPLEMENTED: DEBUGGER');
293                         break;
294
295                         case TOKEN_CONDCOMMENT_MULTILINE:
296                                 $s = $n->value . ' ';
297                                 $childs = $n->treeNodes;
298                                 for ($i = 0, $j = count($childs); $i < $j; $i++)
299                                         $s .= $this->parseTree($childs[$i]);
300                         break;
301
302                         case OP_SEMICOLON:
303                                 if ($expression = $n->expression)
304                                         $s = $this->parseTree($expression);
305                         break;
306
307                         case JS_LABEL:
308                                 $s = $n->label . ':' . $this->parseTree($n->statement);
309                         break;
310
311                         case OP_COMMA:
312                                 $childs = $n->treeNodes;
313                                 for ($i = 0, $j = count($childs); $i < $j; $i++)
314                                         $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
315                         break;
316
317                         case OP_ASSIGN:
318                                 $s = $this->parseTree($n->treeNodes[0]) . $n->value . $this->parseTree($n->treeNodes[1]);
319                         break;
320
321                         case OP_HOOK:
322                                 $s = $this->parseTree($n->treeNodes[0]) . '?' . $this->parseTree($n->treeNodes[1]) . ':' . $this->parseTree($n->treeNodes[2]);
323                         break;
324
325                         case OP_OR: case OP_AND:
326                         case OP_BITWISE_OR: case OP_BITWISE_XOR: case OP_BITWISE_AND:
327                         case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
328                         case OP_LT: case OP_LE: case OP_GE: case OP_GT:
329                         case OP_LSH: case OP_RSH: case OP_URSH:
330                         case OP_MUL: case OP_DIV: case OP_MOD:
331                                 $s = $this->parseTree($n->treeNodes[0]) . $n->type . $this->parseTree($n->treeNodes[1]);
332                         break;
333
334                         case OP_PLUS:
335                         case OP_MINUS:
336                                 $s = $this->parseTree($n->treeNodes[0]) . $n->type;
337                                 $nextTokenType = $n->treeNodes[1]->type;
338                                 if (    $nextTokenType == OP_PLUS || $nextTokenType == OP_MINUS ||
339                                         $nextTokenType == OP_INCREMENT || $nextTokenType == OP_DECREMENT ||
340                                         $nextTokenType == OP_UNARY_PLUS || $nextTokenType == OP_UNARY_MINUS
341                                 )
342                                         $s .= ' ';
343                                 $s .= $this->parseTree($n->treeNodes[1]);
344                         break;
345
346                         case KEYWORD_IN:
347                                 $s = $this->parseTree($n->treeNodes[0]) . ' in ' . $this->parseTree($n->treeNodes[1]);
348                         break;
349
350                         case KEYWORD_INSTANCEOF:
351                                 $s = $this->parseTree($n->treeNodes[0]) . ' instanceof ' . $this->parseTree($n->treeNodes[1]);
352                         break;
353
354                         case KEYWORD_DELETE:
355                                 $s = 'delete ' . $this->parseTree($n->treeNodes[0]);
356                         break;
357
358                         case KEYWORD_VOID:
359                                 $s = 'void(' . $this->parseTree($n->treeNodes[0]) . ')';
360                         break;
361
362                         case KEYWORD_TYPEOF:
363                                 $s = 'typeof ' . $this->parseTree($n->treeNodes[0]);
364                         break;
365
366                         case OP_NOT:
367                         case OP_BITWISE_NOT:
368                         case OP_UNARY_PLUS:
369                         case OP_UNARY_MINUS:
370                                 $s = $n->value . $this->parseTree($n->treeNodes[0]);
371                         break;
372
373                         case OP_INCREMENT:
374                         case OP_DECREMENT:
375                                 if ($n->postfix)
376                                         $s = $this->parseTree($n->treeNodes[0]) . $n->value;
377                                 else
378                                         $s = $n->value . $this->parseTree($n->treeNodes[0]);
379                         break;
380
381                         case OP_DOT:
382                                 $s = $this->parseTree($n->treeNodes[0]) . '.' . $this->parseTree($n->treeNodes[1]);
383                         break;
384
385                         case JS_INDEX:
386                                 $s = $this->parseTree($n->treeNodes[0]);
387                                 // See if we can replace named index with a dot saving 3 bytes
388                                 if (    $n->treeNodes[0]->type == TOKEN_IDENTIFIER &&
389                                         $n->treeNodes[1]->type == TOKEN_STRING &&
390                                         $this->isValidIdentifier(substr($n->treeNodes[1]->value, 1, -1))
391                                 )
392                                         $s .= '.' . substr($n->treeNodes[1]->value, 1, -1);
393                                 else
394                                         $s .= '[' . $this->parseTree($n->treeNodes[1]) . ']';
395                         break;
396
397                         case JS_LIST:
398                                 $childs = $n->treeNodes;
399                                 for ($i = 0, $j = count($childs); $i < $j; $i++)
400                                         $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
401                         break;
402
403                         case JS_CALL:
404                                 $s = $this->parseTree($n->treeNodes[0]) . '(' . $this->parseTree($n->treeNodes[1]) . ')';
405                         break;
406
407                         case KEYWORD_NEW:
408                         case JS_NEW_WITH_ARGS:
409                                 $s = 'new ' . $this->parseTree($n->treeNodes[0]) . '(' . ($n->type == JS_NEW_WITH_ARGS ? $this->parseTree($n->treeNodes[1]) : '') . ')';
410                         break;
411
412                         case JS_ARRAY_INIT:
413                                 $s = '[';
414                                 $childs = $n->treeNodes;
415                                 for ($i = 0, $j = count($childs); $i < $j; $i++)
416                                 {
417                                         $s .= ($i ? ',' : '') . $this->parseTree($childs[$i]);
418                                 }
419                                 $s .= ']';
420                         break;
421
422                         case JS_OBJECT_INIT:
423                                 $s = '{';
424                                 $childs = $n->treeNodes;
425                                 for ($i = 0, $j = count($childs); $i < $j; $i++)
426                                 {
427                                         $t = $childs[$i];
428                                         if ($i)
429                                                 $s .= ',';
430                                         if ($t->type == JS_PROPERTY_INIT)
431                                         {
432                                                 // Ditch the quotes when the index is a valid identifier
433                                                 if (    $t->treeNodes[0]->type == TOKEN_STRING &&
434                                                         $this->isValidIdentifier(substr($t->treeNodes[0]->value, 1, -1))
435                                                 )
436                                                         $s .= substr($t->treeNodes[0]->value, 1, -1);
437                                                 else
438                                                         $s .= $t->treeNodes[0]->value;
439
440                                                 $s .= ':' . $this->parseTree($t->treeNodes[1]);
441                                         }
442                                         else
443                                         {
444                                                 $s .= $t->type == JS_GETTER ? 'get' : 'set';
445                                                 $s .= ' ' . $t->name . '(';
446                                                 $params = $t->params;
447                                                 for ($i = 0, $j = count($params); $i < $j; $i++)
448                                                         $s .= ($i ? ',' : '') . $params[$i];
449                                                 $s .= '){' . $this->parseTree($t->body, true) . '}';
450                                         }
451                                 }
452                                 $s .= '}';
453                         break;
454
455                         case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
456                         case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
457                                 $s = $n->value;
458                         break;
459
460                         case JS_GROUP:
461                                 $s = '(' . $this->parseTree($n->treeNodes[0]) . ')';
462                         break;
463
464                         default:
465                                 throw new Exception('UNKNOWN TOKEN TYPE: ' . $n->type);
466                 }
467
468                 return $s;
469         }
470
471         private function isValidIdentifier($string)
472         {
473                 return preg_match('/^[a-zA-Z_][a-zA-Z0-9_]*$/', $string) && !in_array($string, $this->reserved);
474         }
475 }
476
477 class JSParser
478 {
479         private $t;
480
481         private $opPrecedence = array(
482                 ';' => 0,
483                 ',' => 1,
484                 '=' => 2, '?' => 2, ':' => 2,
485                 // The above all have to have the same precedence, see bug 330975.
486                 '||' => 4,
487                 '&&' => 5,
488                 '|' => 6,
489                 '^' => 7,
490                 '&' => 8,
491                 '==' => 9, '!=' => 9, '===' => 9, '!==' => 9,
492                 '<' => 10, '<=' => 10, '>=' => 10, '>' => 10, 'in' => 10, 'instanceof' => 10,
493                 '<<' => 11, '>>' => 11, '>>>' => 11,
494                 '+' => 12, '-' => 12,
495                 '*' => 13, '/' => 13, '%' => 13,
496                 'delete' => 14, 'void' => 14, 'typeof' => 14,
497                 '!' => 14, '~' => 14, 'U+' => 14, 'U-' => 14,
498                 '++' => 15, '--' => 15,
499                 'new' => 16,
500                 '.' => 17,
501                 JS_NEW_WITH_ARGS => 0, JS_INDEX => 0, JS_CALL => 0,
502                 JS_ARRAY_INIT => 0, JS_OBJECT_INIT => 0, JS_GROUP => 0
503         );
504
505         private $opArity = array(
506                 ',' => -2,
507                 '=' => 2,
508                 '?' => 3,
509                 '||' => 2,
510                 '&&' => 2,
511                 '|' => 2,
512                 '^' => 2,
513                 '&' => 2,
514                 '==' => 2, '!=' => 2, '===' => 2, '!==' => 2,
515                 '<' => 2, '<=' => 2, '>=' => 2, '>' => 2, 'in' => 2, 'instanceof' => 2,
516                 '<<' => 2, '>>' => 2, '>>>' => 2,
517                 '+' => 2, '-' => 2,
518                 '*' => 2, '/' => 2, '%' => 2,
519                 'delete' => 1, 'void' => 1, 'typeof' => 1,
520                 '!' => 1, '~' => 1, 'U+' => 1, 'U-' => 1,
521                 '++' => 1, '--' => 1,
522                 'new' => 1,
523                 '.' => 2,
524                 JS_NEW_WITH_ARGS => 2, JS_INDEX => 2, JS_CALL => 2,
525                 JS_ARRAY_INIT => 1, JS_OBJECT_INIT => 1, JS_GROUP => 1,
526                 TOKEN_CONDCOMMENT_MULTILINE => 1
527         );
528
529         public function __construct()
530         {
531                 $this->t = new JSTokenizer();
532         }
533
534         public function parse($s, $f, $l)
535         {
536                 // initialize tokenizer
537                 $this->t->init($s, $f, $l);
538
539                 $x = new JSCompilerContext(false);
540                 $n = $this->Script($x);
541                 if (!$this->t->isDone())
542                         throw $this->t->newSyntaxError('Syntax error');
543
544                 return $n;
545         }
546
547         private function Script($x)
548         {
549                 $n = $this->Statements($x);
550                 $n->type = JS_SCRIPT;
551                 $n->funDecls = $x->funDecls;
552                 $n->varDecls = $x->varDecls;
553
554                 return $n;
555         }
556
557         private function Statements($x)
558         {
559                 $n = new JSNode($this->t, JS_BLOCK);
560                 array_push($x->stmtStack, $n);
561
562                 while (!$this->t->isDone() && $this->t->peek() != OP_RIGHT_CURLY)
563                         $n->addNode($this->Statement($x));
564
565                 array_pop($x->stmtStack);
566
567                 return $n;
568         }
569
570         private function Block($x)
571         {
572                 $this->t->mustMatch(OP_LEFT_CURLY);
573                 $n = $this->Statements($x);
574                 $this->t->mustMatch(OP_RIGHT_CURLY);
575
576                 return $n;
577         }
578
579         private function Statement($x)
580         {
581                 $tt = $this->t->get();
582                 $n2 = null;
583
584                 // Cases for statements ending in a right curly return early, avoiding the
585                 // common semicolon insertion magic after this switch.
586                 switch ($tt)
587                 {
588                         case KEYWORD_FUNCTION:
589                                 return $this->FunctionDefinition(
590                                         $x,
591                                         true,
592                                         count($x->stmtStack) > 1 ? STATEMENT_FORM : DECLARED_FORM
593                                 );
594                         break;
595
596                         case OP_LEFT_CURLY:
597                                 $n = $this->Statements($x);
598                                 $this->t->mustMatch(OP_RIGHT_CURLY);
599                         return $n;
600
601                         case KEYWORD_IF:
602                                 $n = new JSNode($this->t);
603                                 $n->condition = $this->ParenExpression($x);
604                                 array_push($x->stmtStack, $n);
605                                 $n->thenPart = $this->Statement($x);
606                                 $n->elsePart = $this->t->match(KEYWORD_ELSE) ? $this->Statement($x) : null;
607                                 array_pop($x->stmtStack);
608                         return $n;
609
610                         case KEYWORD_SWITCH:
611                                 $n = new JSNode($this->t);
612                                 $this->t->mustMatch(OP_LEFT_PAREN);
613                                 $n->discriminant = $this->Expression($x);
614                                 $this->t->mustMatch(OP_RIGHT_PAREN);
615                                 $n->cases = array();
616                                 $n->defaultIndex = -1;
617
618                                 array_push($x->stmtStack, $n);
619
620                                 $this->t->mustMatch(OP_LEFT_CURLY);
621
622                                 while (($tt = $this->t->get()) != OP_RIGHT_CURLY)
623                                 {
624                                         switch ($tt)
625                                         {
626                                                 case KEYWORD_DEFAULT:
627                                                         if ($n->defaultIndex >= 0)
628                                                                 throw $this->t->newSyntaxError('More than one switch default');
629                                                         // FALL THROUGH
630                                                 case KEYWORD_CASE:
631                                                         $n2 = new JSNode($this->t);
632                                                         if ($tt == KEYWORD_DEFAULT)
633                                                                 $n->defaultIndex = count($n->cases);
634                                                         else
635                                                                 $n2->caseLabel = $this->Expression($x, OP_COLON);
636                                                                 break;
637                                                 default:
638                                                         throw $this->t->newSyntaxError('Invalid switch case');
639                                         }
640
641                                         $this->t->mustMatch(OP_COLON);
642                                         $n2->statements = new JSNode($this->t, JS_BLOCK);
643                                         while (($tt = $this->t->peek()) != KEYWORD_CASE && $tt != KEYWORD_DEFAULT && $tt != OP_RIGHT_CURLY)
644                                                 $n2->statements->addNode($this->Statement($x));
645
646                                         array_push($n->cases, $n2);
647                                 }
648
649                                 array_pop($x->stmtStack);
650                         return $n;
651
652                         case KEYWORD_FOR:
653                                 $n = new JSNode($this->t);
654                                 $n->isLoop = true;
655                                 $this->t->mustMatch(OP_LEFT_PAREN);
656
657                                 if (($tt = $this->t->peek()) != OP_SEMICOLON)
658                                 {
659                                         $x->inForLoopInit = true;
660                                         if ($tt == KEYWORD_VAR || $tt == KEYWORD_CONST)
661                                         {
662                                                 $this->t->get();
663                                                 $n2 = $this->Variables($x);
664                                         }
665                                         else
666                                         {
667                                                 $n2 = $this->Expression($x);
668                                         }
669                                         $x->inForLoopInit = false;
670                                 }
671
672                                 if ($n2 && $this->t->match(KEYWORD_IN))
673                                 {
674                                         $n->type = JS_FOR_IN;
675                                         if ($n2->type == KEYWORD_VAR)
676                                         {
677                                                 if (count($n2->treeNodes) != 1)
678                                                 {
679                                                         throw $this->t->SyntaxError(
680                                                                 'Invalid for..in left-hand side',
681                                                                 $this->t->filename,
682                                                                 $n2->lineno
683                                                         );
684                                                 }
685
686                                                 // NB: n2[0].type == IDENTIFIER and n2[0].value == n2[0].name.
687                                                 $n->iterator = $n2->treeNodes[0];
688                                                 $n->varDecl = $n2;
689                                         }
690                                         else
691                                         {
692                                                 $n->iterator = $n2;
693                                                 $n->varDecl = null;
694                                         }
695
696                                         $n->object = $this->Expression($x);
697                                 }
698                                 else
699                                 {
700                                         $n->setup = $n2 ? $n2 : null;
701                                         $this->t->mustMatch(OP_SEMICOLON);
702                                         $n->condition = $this->t->peek() == OP_SEMICOLON ? null : $this->Expression($x);
703                                         $this->t->mustMatch(OP_SEMICOLON);
704                                         $n->update = $this->t->peek() == OP_RIGHT_PAREN ? null : $this->Expression($x);
705                                 }
706
707                                 $this->t->mustMatch(OP_RIGHT_PAREN);
708                                 $n->body = $this->nest($x, $n);
709                         return $n;
710
711                         case KEYWORD_WHILE:
712                                 $n = new JSNode($this->t);
713                                 $n->isLoop = true;
714                                 $n->condition = $this->ParenExpression($x);
715                                 $n->body = $this->nest($x, $n);
716                         return $n;
717
718                         case KEYWORD_DO:
719                                 $n = new JSNode($this->t);
720                                 $n->isLoop = true;
721                                 $n->body = $this->nest($x, $n, KEYWORD_WHILE);
722                                 $n->condition = $this->ParenExpression($x);
723                                 if (!$x->ecmaStrictMode)
724                                 {
725                                         // <script language="JavaScript"> (without version hints) may need
726                                         // automatic semicolon insertion without a newline after do-while.
727                                         // See http://bugzilla.mozilla.org/show_bug.cgi?id=238945.
728                                         $this->t->match(OP_SEMICOLON);
729                                         return $n;
730                                 }
731                         break;
732
733                         case KEYWORD_BREAK:
734                         case KEYWORD_CONTINUE:
735                                 $n = new JSNode($this->t);
736
737                                 if ($this->t->peekOnSameLine() == TOKEN_IDENTIFIER)
738                                 {
739                                         $this->t->get();
740                                         $n->label = $this->t->currentToken()->value;
741                                 }
742
743                                 $ss = $x->stmtStack;
744                                 $i = count($ss);
745                                 $label = $n->label;
746                                 if ($label)
747                                 {
748                                         do
749                                         {
750                                                 if (--$i < 0)
751                                                         throw $this->t->newSyntaxError('Label not found');
752                                         }
753                                         while ($ss[$i]->label != $label);
754                                 }
755                                 else
756                                 {
757                                         do
758                                         {
759                                                 if (--$i < 0)
760                                                         throw $this->t->newSyntaxError('Invalid ' . $tt);
761                                         }
762                                         while (!$ss[$i]->isLoop && ($tt != KEYWORD_BREAK || $ss[$i]->type != KEYWORD_SWITCH));
763                                 }
764
765                                 $n->target = $ss[$i];
766                         break;
767
768                         case KEYWORD_TRY:
769                                 $n = new JSNode($this->t);
770                                 $n->tryBlock = $this->Block($x);
771                                 $n->catchClauses = array();
772
773                                 while ($this->t->match(KEYWORD_CATCH))
774                                 {
775                                         $n2 = new JSNode($this->t);
776                                         $this->t->mustMatch(OP_LEFT_PAREN);
777                                         $n2->varName = $this->t->mustMatch(TOKEN_IDENTIFIER)->value;
778
779                                         if ($this->t->match(KEYWORD_IF))
780                                         {
781                                                 if ($x->ecmaStrictMode)
782                                                         throw $this->t->newSyntaxError('Illegal catch guard');
783
784                                                 if (count($n->catchClauses) && !end($n->catchClauses)->guard)
785                                                         throw $this->t->newSyntaxError('Guarded catch after unguarded');
786
787                                                 $n2->guard = $this->Expression($x);
788                                         }
789                                         else
790                                         {
791                                                 $n2->guard = null;
792                                         }
793
794                                         $this->t->mustMatch(OP_RIGHT_PAREN);
795                                         $n2->block = $this->Block($x);
796                                         array_push($n->catchClauses, $n2);
797                                 }
798
799                                 if ($this->t->match(KEYWORD_FINALLY))
800                                         $n->finallyBlock = $this->Block($x);
801
802                                 if (!count($n->catchClauses) && !$n->finallyBlock)
803                                         throw $this->t->newSyntaxError('Invalid try statement');
804                         return $n;
805
806                         case KEYWORD_CATCH:
807                         case KEYWORD_FINALLY:
808                                 throw $this->t->newSyntaxError($tt + ' without preceding try');
809
810                         case KEYWORD_THROW:
811                                 $n = new JSNode($this->t);
812                                 $n->exception = $this->Expression($x);
813                         break;
814
815                         case KEYWORD_RETURN:
816                                 if (!$x->inFunction)
817                                         throw $this->t->newSyntaxError('Invalid return');
818
819                                 $n = new JSNode($this->t);
820                                 $tt = $this->t->peekOnSameLine();
821                                 if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
822                                         $n->value = $this->Expression($x);
823                                 else
824                                         $n->value = null;
825                         break;
826
827                         case KEYWORD_WITH:
828                                 $n = new JSNode($this->t);
829                                 $n->object = $this->ParenExpression($x);
830                                 $n->body = $this->nest($x, $n);
831                         return $n;
832
833                         case KEYWORD_VAR:
834                         case KEYWORD_CONST:
835                                 $n = $this->Variables($x);
836                         break;
837
838                         case TOKEN_CONDCOMMENT_MULTILINE:
839                                 $n = new JSNode($this->t);
840                         return $n;
841
842                         case KEYWORD_DEBUGGER:
843                                 $n = new JSNode($this->t);
844                         break;
845
846                         case TOKEN_NEWLINE:
847                         case OP_SEMICOLON:
848                                 $n = new JSNode($this->t, OP_SEMICOLON);
849                                 $n->expression = null;
850                         return $n;
851
852                         default:
853                                 if ($tt == TOKEN_IDENTIFIER)
854                                 {
855                                         $this->t->scanOperand = false;
856                                         $tt = $this->t->peek();
857                                         $this->t->scanOperand = true;
858                                         if ($tt == OP_COLON)
859                                         {
860                                                 $label = $this->t->currentToken()->value;
861                                                 $ss = $x->stmtStack;
862                                                 for ($i = count($ss) - 1; $i >= 0; --$i)
863                                                 {
864                                                         if ($ss[$i]->label == $label)
865                                                                 throw $this->t->newSyntaxError('Duplicate label');
866                                                 }
867
868                                                 $this->t->get();
869                                                 $n = new JSNode($this->t, JS_LABEL);
870                                                 $n->label = $label;
871                                                 $n->statement = $this->nest($x, $n);
872
873                                                 return $n;
874                                         }
875                                 }
876
877                                 $n = new JSNode($this->t, OP_SEMICOLON);
878                                 $this->t->unget();
879                                 $n->expression = $this->Expression($x);
880                                 $n->end = $n->expression->end;
881                         break;
882                 }
883
884                 if ($this->t->lineno == $this->t->currentToken()->lineno)
885                 {
886                         $tt = $this->t->peekOnSameLine();
887                         if ($tt != TOKEN_END && $tt != TOKEN_NEWLINE && $tt != OP_SEMICOLON && $tt != OP_RIGHT_CURLY)
888                                 throw $this->t->newSyntaxError('Missing ; before statement');
889                 }
890
891                 $this->t->match(OP_SEMICOLON);
892
893                 return $n;
894         }
895
896         private function FunctionDefinition($x, $requireName, $functionForm)
897         {
898                 $f = new JSNode($this->t);
899
900                 if ($f->type != KEYWORD_FUNCTION)
901                         $f->type = ($f->value == 'get') ? JS_GETTER : JS_SETTER;
902
903                 if ($this->t->match(TOKEN_IDENTIFIER))
904                         $f->name = $this->t->currentToken()->value;
905                 elseif ($requireName)
906                         throw $this->t->newSyntaxError('Missing function identifier');
907
908                 $this->t->mustMatch(OP_LEFT_PAREN);
909                         $f->params = array();
910
911                 while (($tt = $this->t->get()) != OP_RIGHT_PAREN)
912                 {
913                         if ($tt != TOKEN_IDENTIFIER)
914                                 throw $this->t->newSyntaxError('Missing formal parameter');
915
916                         array_push($f->params, $this->t->currentToken()->value);
917
918                         if ($this->t->peek() != OP_RIGHT_PAREN)
919                                 $this->t->mustMatch(OP_COMMA);
920                 }
921
922                 $this->t->mustMatch(OP_LEFT_CURLY);
923
924                 $x2 = new JSCompilerContext(true);
925                 $f->body = $this->Script($x2);
926
927                 $this->t->mustMatch(OP_RIGHT_CURLY);
928                 $f->end = $this->t->currentToken()->end;
929
930                 $f->functionForm = $functionForm;
931                 if ($functionForm == DECLARED_FORM)
932                         array_push($x->funDecls, $f);
933
934                 return $f;
935         }
936
937         private function Variables($x)
938         {
939                 $n = new JSNode($this->t);
940
941                 do
942                 {
943                         $this->t->mustMatch(TOKEN_IDENTIFIER);
944
945                         $n2 = new JSNode($this->t);
946                         $n2->name = $n2->value;
947
948                         if ($this->t->match(OP_ASSIGN))
949                         {
950                                 if ($this->t->currentToken()->assignOp)
951                                         throw $this->t->newSyntaxError('Invalid variable initialization');
952
953                                 $n2->initializer = $this->Expression($x, OP_COMMA);
954                         }
955
956                         $n2->readOnly = $n->type == KEYWORD_CONST;
957
958                         $n->addNode($n2);
959                         array_push($x->varDecls, $n2);
960                 }
961                 while ($this->t->match(OP_COMMA));
962
963                 return $n;
964         }
965
966         private function Expression($x, $stop=false)
967         {
968                 $operators = array();
969                 $operands = array();
970                 $n = false;
971
972                 $bl = $x->bracketLevel;
973                 $cl = $x->curlyLevel;
974                 $pl = $x->parenLevel;
975                 $hl = $x->hookLevel;
976
977                 while (($tt = $this->t->get()) != TOKEN_END)
978                 {
979                         if ($tt == $stop &&
980                                 $x->bracketLevel == $bl &&
981                                 $x->curlyLevel == $cl &&
982                                 $x->parenLevel == $pl &&
983                                 $x->hookLevel == $hl
984                         )
985                         {
986                                 // Stop only if tt matches the optional stop parameter, and that
987                                 // token is not quoted by some kind of bracket.
988                                 break;
989                         }
990
991                         switch ($tt)
992                         {
993                                 case OP_SEMICOLON:
994                                         // NB: cannot be empty, Statement handled that.
995                                         break 2;
996
997                                 case OP_ASSIGN:
998                                 case OP_HOOK:
999                                 case OP_COLON:
1000                                         if ($this->t->scanOperand)
1001                                                 break 2;
1002
1003                                         // Use >, not >=, for right-associative ASSIGN and HOOK/COLON.
1004                                         while ( !empty($operators) &&
1005                                                 (       $this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt] ||
1006                                                         ($tt == OP_COLON && end($operators)->type == OP_ASSIGN)
1007                                                 )
1008                                         )
1009                                                 $this->reduce($operators, $operands);
1010
1011                                         if ($tt == OP_COLON)
1012                                         {
1013                                                 $n = end($operators);
1014                                                 if ($n->type != OP_HOOK)
1015                                                         throw $this->t->newSyntaxError('Invalid label');
1016
1017                                                 --$x->hookLevel;
1018                                         }
1019                                         else
1020                                         {
1021                                                 array_push($operators, new JSNode($this->t));
1022                                                 if ($tt == OP_ASSIGN)
1023                                                         end($operands)->assignOp = $this->t->currentToken()->assignOp;
1024                                                 else
1025                                                         ++$x->hookLevel;
1026                                         }
1027
1028                                         $this->t->scanOperand = true;
1029                                 break;
1030
1031                                 case KEYWORD_IN:
1032                                         // An in operator should not be parsed if we're parsing the head of
1033                                         // a for (...) loop, unless it is in the then part of a conditional
1034                                         // expression, or parenthesized somehow.
1035                                         if ($x->inForLoopInit && !$x->hookLevel &&
1036                                                 !$x->bracketLevel && !$x->curlyLevel &&
1037                                                 !$x->parenLevel
1038                                         )
1039                                         {
1040                                                 break 2;
1041                                         }
1042                                 // FALL THROUGH
1043                                 case OP_COMMA:
1044                                         // Treat comma as left-associative so reduce can fold left-heavy
1045                                         // COMMA trees into a single array.
1046                                         // FALL THROUGH
1047                                 case OP_OR:
1048                                 case OP_AND:
1049                                 case OP_BITWISE_OR:
1050                                 case OP_BITWISE_XOR:
1051                                 case OP_BITWISE_AND:
1052                                 case OP_EQ: case OP_NE: case OP_STRICT_EQ: case OP_STRICT_NE:
1053                                 case OP_LT: case OP_LE: case OP_GE: case OP_GT:
1054                                 case KEYWORD_INSTANCEOF:
1055                                 case OP_LSH: case OP_RSH: case OP_URSH:
1056                                 case OP_PLUS: case OP_MINUS:
1057                                 case OP_MUL: case OP_DIV: case OP_MOD:
1058                                 case OP_DOT:
1059                                         if ($this->t->scanOperand)
1060                                                 break 2;
1061
1062                                         while ( !empty($operators) &&
1063                                                 $this->opPrecedence[end($operators)->type] >= $this->opPrecedence[$tt]
1064                                         )
1065                                                 $this->reduce($operators, $operands);
1066
1067                                         if ($tt == OP_DOT)
1068                                         {
1069                                                 $this->t->mustMatch(TOKEN_IDENTIFIER);
1070                                                 array_push($operands, new JSNode($this->t, OP_DOT, array_pop($operands), new JSNode($this->t)));
1071                                         }
1072                                         else
1073                                         {
1074                                                 array_push($operators, new JSNode($this->t));
1075                                                 $this->t->scanOperand = true;
1076                                         }
1077                                 break;
1078
1079                                 case KEYWORD_DELETE: case KEYWORD_VOID: case KEYWORD_TYPEOF:
1080                                 case OP_NOT: case OP_BITWISE_NOT: case OP_UNARY_PLUS: case OP_UNARY_MINUS:
1081                                 case KEYWORD_NEW:
1082                                         if (!$this->t->scanOperand)
1083                                                 break 2;
1084
1085                                         array_push($operators, new JSNode($this->t));
1086                                 break;
1087
1088                                 case OP_INCREMENT: case OP_DECREMENT:
1089                                         if ($this->t->scanOperand)
1090                                         {
1091                                                 array_push($operators, new JSNode($this->t));  // prefix increment or decrement
1092                                         }
1093                                         else
1094                                         {
1095                                                 // Don't cross a line boundary for postfix {in,de}crement.
1096                                                 $t = $this->t->tokens[($this->t->tokenIndex + $this->t->lookahead - 1) & 3];
1097                                                 if ($t && $t->lineno != $this->t->lineno)
1098                                                         break 2;
1099
1100                                                 if (!empty($operators))
1101                                                 {
1102                                                         // Use >, not >=, so postfix has higher precedence than prefix.
1103                                                         while ($this->opPrecedence[end($operators)->type] > $this->opPrecedence[$tt])
1104                                                                 $this->reduce($operators, $operands);
1105                                                 }
1106
1107                                                 $n = new JSNode($this->t, $tt, array_pop($operands));
1108                                                 $n->postfix = true;
1109                                                 array_push($operands, $n);
1110                                         }
1111                                 break;
1112
1113                                 case KEYWORD_FUNCTION:
1114                                         if (!$this->t->scanOperand)
1115                                                 break 2;
1116
1117                                         array_push($operands, $this->FunctionDefinition($x, false, EXPRESSED_FORM));
1118                                         $this->t->scanOperand = false;
1119                                 break;
1120
1121                                 case KEYWORD_NULL: case KEYWORD_THIS: case KEYWORD_TRUE: case KEYWORD_FALSE:
1122                                 case TOKEN_IDENTIFIER: case TOKEN_NUMBER: case TOKEN_STRING: case TOKEN_REGEXP:
1123                                         if (!$this->t->scanOperand)
1124                                                 break 2;
1125
1126                                         array_push($operands, new JSNode($this->t));
1127                                         $this->t->scanOperand = false;
1128                                 break;
1129
1130                                 case TOKEN_CONDCOMMENT_MULTILINE:
1131                                         if ($this->t->scanOperand)
1132                                                 array_push($operators, new JSNode($this->t));
1133                                         else
1134                                                 array_push($operands, new JSNode($this->t));
1135                                 break;
1136
1137                                 case OP_LEFT_BRACKET:
1138                                         if ($this->t->scanOperand)
1139                                         {
1140                                                 // Array initialiser.  Parse using recursive descent, as the
1141                                                 // sub-grammar here is not an operator grammar.
1142                                                 $n = new JSNode($this->t, JS_ARRAY_INIT);
1143                                                 while (($tt = $this->t->peek()) != OP_RIGHT_BRACKET)
1144                                                 {
1145                                                         if ($tt == OP_COMMA)
1146                                                         {
1147                                                                 $this->t->get();
1148                                                                 $n->addNode(null);
1149                                                                 continue;
1150                                                         }
1151
1152                                                         $n->addNode($this->Expression($x, OP_COMMA));
1153                                                         if (!$this->t->match(OP_COMMA))
1154                                                                 break;
1155                                                 }
1156
1157                                                 $this->t->mustMatch(OP_RIGHT_BRACKET);
1158                                                 array_push($operands, $n);
1159                                                 $this->t->scanOperand = false;
1160                                         }
1161                                         else
1162                                         {
1163                                                 // Property indexing operator.
1164                                                 array_push($operators, new JSNode($this->t, JS_INDEX));
1165                                                 $this->t->scanOperand = true;
1166                                                 ++$x->bracketLevel;
1167                                         }
1168                                 break;
1169
1170                                 case OP_RIGHT_BRACKET:
1171                                         if ($this->t->scanOperand || $x->bracketLevel == $bl)
1172                                                 break 2;
1173
1174                                         while ($this->reduce($operators, $operands)->type != JS_INDEX)
1175                                                 continue;
1176
1177                                         --$x->bracketLevel;
1178                                 break;
1179
1180                                 case OP_LEFT_CURLY:
1181                                         if (!$this->t->scanOperand)
1182                                                 break 2;
1183
1184                                         // Object initialiser.  As for array initialisers (see above),
1185                                         // parse using recursive descent.
1186                                         ++$x->curlyLevel;
1187                                         $n = new JSNode($this->t, JS_OBJECT_INIT);
1188                                         while (!$this->t->match(OP_RIGHT_CURLY))
1189                                         {
1190                                                 do
1191                                                 {
1192                                                         $tt = $this->t->get();
1193                                                         $tv = $this->t->currentToken()->value;
1194                                                         if (($tv == 'get' || $tv == 'set') && $this->t->peek() == TOKEN_IDENTIFIER)
1195                                                         {
1196                                                                 if ($x->ecmaStrictMode)
1197                                                                         throw $this->t->newSyntaxError('Illegal property accessor');
1198
1199                                                                 $n->addNode($this->FunctionDefinition($x, true, EXPRESSED_FORM));
1200                                                         }
1201                                                         else
1202                                                         {
1203                                                                 switch ($tt)
1204                                                                 {
1205                                                                         case TOKEN_IDENTIFIER:
1206                                                                         case TOKEN_NUMBER:
1207                                                                         case TOKEN_STRING:
1208                                                                                 $id = new JSNode($this->t);
1209                                                                         break;
1210
1211                                                                         case OP_RIGHT_CURLY:
1212                                                                                 if ($x->ecmaStrictMode)
1213                                                                                         throw $this->t->newSyntaxError('Illegal trailing ,');
1214                                                                         break 3;
1215
1216                                                                         default:
1217                                                                                 throw $this->t->newSyntaxError('Invalid property name');
1218                                                                 }
1219
1220                                                                 $this->t->mustMatch(OP_COLON);
1221                                                                 $n->addNode(new JSNode($this->t, JS_PROPERTY_INIT, $id, $this->Expression($x, OP_COMMA)));
1222                                                         }
1223                                                 }
1224                                                 while ($this->t->match(OP_COMMA));
1225
1226                                                 $this->t->mustMatch(OP_RIGHT_CURLY);
1227                                                 break;
1228                                         }
1229
1230                                         array_push($operands, $n);
1231                                         $this->t->scanOperand = false;
1232                                         --$x->curlyLevel;
1233                                 break;
1234
1235                                 case OP_RIGHT_CURLY:
1236                                         if (!$this->t->scanOperand && $x->curlyLevel != $cl)
1237                                                 throw new Exception('PANIC: right curly botch');
1238                                 break 2;
1239
1240                                 case OP_LEFT_PAREN:
1241                                         if ($this->t->scanOperand)
1242                                         {
1243                                                 array_push($operators, new JSNode($this->t, JS_GROUP));
1244                                         }
1245                                         else
1246                                         {
1247                                                 while ( !empty($operators) &&
1248                                                         $this->opPrecedence[end($operators)->type] > $this->opPrecedence[KEYWORD_NEW]
1249                                                 )
1250                                                         $this->reduce($operators, $operands);
1251
1252                                                 // Handle () now, to regularize the n-ary case for n > 0.
1253                                                 // We must set scanOperand in case there are arguments and
1254                                                 // the first one is a regexp or unary+/-.
1255                                                 $n = end($operators);
1256                                                 $this->t->scanOperand = true;
1257                                                 if ($this->t->match(OP_RIGHT_PAREN))
1258                                                 {
1259                                                         if ($n && $n->type == KEYWORD_NEW)
1260                                                         {
1261                                                                 array_pop($operators);
1262                                                                 $n->addNode(array_pop($operands));
1263                                                         }
1264                                                         else
1265                                                         {
1266                                                                 $n = new JSNode($this->t, JS_CALL, array_pop($operands), new JSNode($this->t, JS_LIST));
1267                                                         }
1268
1269                                                         array_push($operands, $n);
1270                                                         $this->t->scanOperand = false;
1271                                                         break;
1272                                                 }
1273
1274                                                 if ($n && $n->type == KEYWORD_NEW)
1275                                                         $n->type = JS_NEW_WITH_ARGS;
1276                                                 else
1277                                                         array_push($operators, new JSNode($this->t, JS_CALL));
1278                                         }
1279
1280                                         ++$x->parenLevel;
1281                                 break;
1282
1283                                 case OP_RIGHT_PAREN:
1284                                         if ($this->t->scanOperand || $x->parenLevel == $pl)
1285                                                 break 2;
1286
1287                                         while (($tt = $this->reduce($operators, $operands)->type) != JS_GROUP &&
1288                                                 $tt != JS_CALL && $tt != JS_NEW_WITH_ARGS
1289                                         )
1290                                         {
1291                                                 continue;
1292                                         }
1293
1294                                         if ($tt != JS_GROUP)
1295                                         {
1296                                                 $n = end($operands);
1297                                                 if ($n->treeNodes[1]->type != OP_COMMA)
1298                                                         $n->treeNodes[1] = new JSNode($this->t, JS_LIST, $n->treeNodes[1]);
1299                                                 else
1300                                                         $n->treeNodes[1]->type = JS_LIST;
1301                                         }
1302
1303                                         --$x->parenLevel;
1304                                 break;
1305
1306                                 // Automatic semicolon insertion means we may scan across a newline
1307                                 // and into the beginning of another statement.  If so, break out of
1308                                 // the while loop and let the t.scanOperand logic handle errors.
1309                                 default:
1310                                         break 2;
1311                         }
1312                 }
1313
1314                 if ($x->hookLevel != $hl)
1315                         throw $this->t->newSyntaxError('Missing : after ?');
1316
1317                 if ($x->parenLevel != $pl)
1318                         throw $this->t->newSyntaxError('Missing ) in parenthetical');
1319
1320                 if ($x->bracketLevel != $bl)
1321                         throw $this->t->newSyntaxError('Missing ] in index expression');
1322
1323                 if ($this->t->scanOperand)
1324                         throw $this->t->newSyntaxError('Missing operand');
1325
1326                 // Resume default mode, scanning for operands, not operators.
1327                 $this->t->scanOperand = true;
1328                 $this->t->unget();
1329
1330                 while (count($operators))
1331                         $this->reduce($operators, $operands);
1332
1333                 return array_pop($operands);
1334         }
1335
1336         private function ParenExpression($x)
1337         {
1338                 $this->t->mustMatch(OP_LEFT_PAREN);
1339                 $n = $this->Expression($x);
1340                 $this->t->mustMatch(OP_RIGHT_PAREN);
1341
1342                 return $n;
1343         }
1344
1345         // Statement stack and nested statement handler.
1346         private function nest($x, $node, $end = false)
1347         {
1348                 array_push($x->stmtStack, $node);
1349                 $n = $this->statement($x);
1350                 array_pop($x->stmtStack);
1351
1352                 if ($end)
1353                         $this->t->mustMatch($end);
1354
1355                 return $n;
1356         }
1357
1358         private function reduce(&$operators, &$operands)
1359         {
1360                 $n = array_pop($operators);
1361                 $op = $n->type;
1362                 $arity = $this->opArity[$op];
1363                 $c = count($operands);
1364                 if ($arity == -2)
1365                 {
1366                         // Flatten left-associative trees
1367                         if ($c >= 2)
1368                         {
1369                                 $left = $operands[$c - 2];
1370                                 if ($left->type == $op)
1371                                 {
1372                                         $right = array_pop($operands);
1373                                         $left->addNode($right);
1374                                         return $left;
1375                                 }
1376                         }
1377                         $arity = 2;
1378                 }
1379
1380                 // Always use push to add operands to n, to update start and end
1381                 $a = array_splice($operands, $c - $arity);
1382                 for ($i = 0; $i < $arity; $i++)
1383                         $n->addNode($a[$i]);
1384
1385                 // Include closing bracket or postfix operator in [start,end]
1386                 $te = $this->t->currentToken()->end;
1387                 if ($n->end < $te)
1388                         $n->end = $te;
1389
1390                 array_push($operands, $n);
1391
1392                 return $n;
1393         }
1394 }
1395
1396 class JSCompilerContext
1397 {
1398         public $inFunction = false;
1399         public $inForLoopInit = false;
1400         public $ecmaStrictMode = false;
1401         public $bracketLevel = 0;
1402         public $curlyLevel = 0;
1403         public $parenLevel = 0;
1404         public $hookLevel = 0;
1405
1406         public $stmtStack = array();
1407         public $funDecls = array();
1408         public $varDecls = array();
1409
1410         public function __construct($inFunction)
1411         {
1412                 $this->inFunction = $inFunction;
1413         }
1414 }
1415
1416 class JSNode
1417 {
1418         private $type;
1419         private $value;
1420         private $lineno;
1421         private $start;
1422         private $end;
1423
1424         public $treeNodes = array();
1425         public $funDecls = array();
1426         public $varDecls = array();
1427
1428         public function __construct($t, $type=0)
1429         {
1430                 if ($token = $t->currentToken())
1431                 {
1432                         $this->type = $type ? $type : $token->type;
1433                         $this->value = $token->value;
1434                         $this->lineno = $token->lineno;
1435                         $this->start = $token->start;
1436                         $this->end = $token->end;
1437                 }
1438                 else
1439                 {
1440                         $this->type = $type;
1441                         $this->lineno = $t->lineno;
1442                 }
1443
1444                 if (($numargs = func_num_args()) > 2)
1445                 {
1446                         $args = func_get_args();;
1447                         for ($i = 2; $i < $numargs; $i++)
1448                                 $this->addNode($args[$i]);
1449                 }
1450         }
1451
1452         // we don't want to bloat our object with all kind of specific properties, so we use overloading
1453         public function __set($name, $value)
1454         {
1455                 $this->$name = $value;
1456         }
1457
1458         public function __get($name)
1459         {
1460                 if (isset($this->$name))
1461                         return $this->$name;
1462
1463                 return null;
1464         }
1465
1466         public function addNode($node)
1467         {
1468                 $this->treeNodes[] = $node;
1469         }
1470 }
1471
1472 class JSTokenizer
1473 {
1474         private $cursor = 0;
1475         private $source;
1476
1477         public $tokens = array();
1478         public $tokenIndex = 0;
1479         public $lookahead = 0;
1480         public $scanNewlines = false;
1481         public $scanOperand = true;
1482
1483         public $filename;
1484         public $lineno;
1485
1486         private $keywords = array(
1487                 'break',
1488                 'case', 'catch', 'const', 'continue',
1489                 'debugger', 'default', 'delete', 'do',
1490                 'else', 'enum',
1491                 'false', 'finally', 'for', 'function',
1492                 'if', 'in', 'instanceof',
1493                 'new', 'null',
1494                 'return',
1495                 'switch',
1496                 'this', 'throw', 'true', 'try', 'typeof',
1497                 'var', 'void',
1498                 'while', 'with'
1499         );
1500
1501         private $opTypeNames = array(
1502                 ';'     => 'SEMICOLON',
1503                 ','     => 'COMMA',
1504                 '?'     => 'HOOK',
1505                 ':'     => 'COLON',
1506                 '||'    => 'OR',
1507                 '&&'    => 'AND',
1508                 '|'     => 'BITWISE_OR',
1509                 '^'     => 'BITWISE_XOR',
1510                 '&'     => 'BITWISE_AND',
1511                 '==='   => 'STRICT_EQ',
1512                 '=='    => 'EQ',
1513                 '='     => 'ASSIGN',
1514                 '!=='   => 'STRICT_NE',
1515                 '!='    => 'NE',
1516                 '<<'    => 'LSH',
1517                 '<='    => 'LE',
1518                 '<'     => 'LT',
1519                 '>>>'   => 'URSH',
1520                 '>>'    => 'RSH',
1521                 '>='    => 'GE',
1522                 '>'     => 'GT',
1523                 '++'    => 'INCREMENT',
1524                 '--'    => 'DECREMENT',
1525                 '+'     => 'PLUS',
1526                 '-'     => 'MINUS',
1527                 '*'     => 'MUL',
1528                 '/'     => 'DIV',
1529                 '%'     => 'MOD',
1530                 '!'     => 'NOT',
1531                 '~'     => 'BITWISE_NOT',
1532                 '.'     => 'DOT',
1533                 '['     => 'LEFT_BRACKET',
1534                 ']'     => 'RIGHT_BRACKET',
1535                 '{'     => 'LEFT_CURLY',
1536                 '}'     => 'RIGHT_CURLY',
1537                 '('     => 'LEFT_PAREN',
1538                 ')'     => 'RIGHT_PAREN',
1539                 '@*/'   => 'CONDCOMMENT_END'
1540         );
1541
1542         private $assignOps = array('|', '^', '&', '<<', '>>', '>>>', '+', '-', '*', '/', '%');
1543         private $opRegExp;
1544
1545         public function __construct()
1546         {
1547                 $this->opRegExp = '#^(' . implode('|', array_map('preg_quote', array_keys($this->opTypeNames))) . ')#';
1548
1549                 // this is quite a hidden yet convenient place to create the defines for operators and keywords
1550                 foreach ($this->opTypeNames as $operand => $name)
1551                         define('OP_' . $name, $operand);
1552
1553                 define('OP_UNARY_PLUS', 'U+');
1554                 define('OP_UNARY_MINUS', 'U-');
1555
1556                 foreach ($this->keywords as $keyword)
1557                         define('KEYWORD_' . strtoupper($keyword), $keyword);
1558         }
1559
1560         public function init($source, $filename = '', $lineno = 1)
1561         {
1562                 $this->source = $source;
1563                 $this->filename = $filename ? $filename : '[inline]';
1564                 $this->lineno = $lineno;
1565
1566                 $this->cursor = 0;
1567                 $this->tokens = array();
1568                 $this->tokenIndex = 0;
1569                 $this->lookahead = 0;
1570                 $this->scanNewlines = false;
1571                 $this->scanOperand = true;
1572         }
1573
1574         public function getInput($chunksize)
1575         {
1576                 if ($chunksize)
1577                         return substr($this->source, $this->cursor, $chunksize);
1578
1579                 return substr($this->source, $this->cursor);
1580         }
1581
1582         public function isDone()
1583         {
1584                 return $this->peek() == TOKEN_END;
1585         }
1586
1587         public function match($tt)
1588         {
1589                 return $this->get() == $tt || $this->unget();
1590         }
1591
1592         public function mustMatch($tt)
1593         {
1594                 if (!$this->match($tt))
1595                         throw $this->newSyntaxError('Unexpected token; token ' . $tt . ' expected');
1596
1597                 return $this->currentToken();
1598         }
1599
1600         public function peek()
1601         {
1602                 if ($this->lookahead)
1603                 {
1604                         $next = $this->tokens[($this->tokenIndex + $this->lookahead) & 3];
1605                         if ($this->scanNewlines && $next->lineno != $this->lineno)
1606                                 $tt = TOKEN_NEWLINE;
1607                         else
1608                                 $tt = $next->type;
1609                 }
1610                 else
1611                 {
1612                         $tt = $this->get();
1613                         $this->unget();
1614                 }
1615
1616                 return $tt;
1617         }
1618
1619         public function peekOnSameLine()
1620         {
1621                 $this->scanNewlines = true;
1622                 $tt = $this->peek();
1623                 $this->scanNewlines = false;
1624
1625                 return $tt;
1626         }
1627
1628         public function currentToken()
1629         {
1630                 if (!empty($this->tokens))
1631                         return $this->tokens[$this->tokenIndex];
1632         }
1633
1634         public function get($chunksize = 1000)
1635         {
1636                 while($this->lookahead)
1637                 {
1638                         $this->lookahead--;
1639                         $this->tokenIndex = ($this->tokenIndex + 1) & 3;
1640                         $token = $this->tokens[$this->tokenIndex];
1641                         if ($token->type != TOKEN_NEWLINE || $this->scanNewlines)
1642                                 return $token->type;
1643                 }
1644
1645                 $conditional_comment = false;
1646
1647                 // strip whitespace and comments
1648                 while(true)
1649                 {
1650                         $input = $this->getInput($chunksize);
1651
1652                         // whitespace handling; gobble up \r as well (effectively we don't have support for MAC newlines!)
1653                         $re = $this->scanNewlines ? '/^[ \r\t]+/' : '/^\s+/';
1654                         if (preg_match($re, $input, $match))
1655                         {
1656                                 $spaces = $match[0];
1657                                 $spacelen = strlen($spaces);
1658                                 $this->cursor += $spacelen;
1659                                 if (!$this->scanNewlines)
1660                                         $this->lineno += substr_count($spaces, "\n");
1661
1662                                 if ($spacelen == $chunksize)
1663                                         continue; // complete chunk contained whitespace
1664
1665                                 $input = $this->getInput($chunksize);
1666                                 if ($input == '' || $input[0] != '/')
1667                                         break;
1668                         }
1669
1670                         // Comments
1671                         if (!preg_match('/^\/(?:\*(@(?:cc_on|if|elif|else|end))?(?:.|\n)*?\*\/|\/.*)/', $input, $match))
1672                         {
1673                                 if (!$chunksize)
1674                                         break;
1675
1676                                 // retry with a full chunk fetch; this also prevents breakage of long regular expressions (which will never match a comment)
1677                                 $chunksize = null;
1678                                 continue;
1679                         }
1680
1681                         // check if this is a conditional (JScript) comment
1682                         if (!empty($match[1]))
1683                         {
1684                                 //$match[0] = '/*' . $match[1];
1685                                 $conditional_comment = true;
1686                                 break;
1687                         }
1688                         else
1689                         {
1690                                 $this->cursor += strlen($match[0]);
1691                                 $this->lineno += substr_count($match[0], "\n");
1692                         }
1693                 }
1694
1695                 if ($input == '')
1696                 {
1697                         $tt = TOKEN_END;
1698                         $match = array('');
1699                 }
1700                 elseif ($conditional_comment)
1701                 {
1702                         $tt = TOKEN_CONDCOMMENT_MULTILINE;
1703                 }
1704                 else
1705                 {
1706                         switch ($input[0])
1707                         {
1708                                 case '0': case '1': case '2': case '3': case '4':
1709                                 case '5': case '6': case '7': case '8': case '9':
1710                                         if (preg_match('/^\d+\.\d*(?:[eE][-+]?\d+)?|^\d+(?:\.\d*)?[eE][-+]?\d+/', $input, $match))
1711                                         {
1712                                                 $tt = TOKEN_NUMBER;
1713                                         }
1714                                         elseif (preg_match('/^0[xX][\da-fA-F]+|^0[0-7]*|^\d+/', $input, $match))
1715                                         {
1716                                                 // this should always match because of \d+
1717                                                 $tt = TOKEN_NUMBER;
1718                                         }
1719                                 break;
1720
1721                                 case '"':
1722                                 case "'":
1723                                         if (preg_match('/^"(?:\\\\(?:.|\r?\n)|[^\\\\"\r\n])*"|^\'(?:\\\\(?:.|\r?\n)|[^\\\\\'\r\n])*\'/', $input, $match))
1724                                         {
1725                                                 $tt = TOKEN_STRING;
1726                                         }
1727                                         else
1728                                         {
1729                                                 if ($chunksize)
1730                                                         return $this->get(null); // retry with a full chunk fetch
1731
1732                                                 throw $this->newSyntaxError('Unterminated string literal');
1733                                         }
1734                                 break;
1735
1736                                 case '/':
1737                                         if ($this->scanOperand && preg_match('/^\/((?:\\\\.|\[(?:\\\\.|[^\]])*\]|[^\/])+)\/([gimy]*)/', $input, $match))
1738                                         {
1739                                                 $tt = TOKEN_REGEXP;
1740                                                 break;
1741                                         }
1742                                 // fall through
1743
1744                                 case '|':
1745                                 case '^':
1746                                 case '&':
1747                                 case '<':
1748                                 case '>':
1749                                 case '+':
1750                                 case '-':
1751                                 case '*':
1752                                 case '%':
1753                                 case '=':
1754                                 case '!':
1755                                         // should always match
1756                                         preg_match($this->opRegExp, $input, $match);
1757                                         $op = $match[0];
1758                                         if (in_array($op, $this->assignOps) && $input[strlen($op)] == '=')
1759                                         {
1760                                                 $tt = OP_ASSIGN;
1761                                                 $match[0] .= '=';
1762                                         }
1763                                         else
1764                                         {
1765                                                 $tt = $op;
1766                                                 if ($this->scanOperand)
1767                                                 {
1768                                                         if ($op == OP_PLUS)
1769                                                                 $tt = OP_UNARY_PLUS;
1770                                                         elseif ($op == OP_MINUS)
1771                                                                 $tt = OP_UNARY_MINUS;
1772                                                 }
1773                                                 $op = null;
1774                                         }
1775                                 break;
1776
1777                                 case '.':
1778                                         if (preg_match('/^\.\d+(?:[eE][-+]?\d+)?/', $input, $match))
1779                                         {
1780                                                 $tt = TOKEN_NUMBER;
1781                                                 break;
1782                                         }
1783                                 // fall through
1784
1785                                 case ';':
1786                                 case ',':
1787                                 case '?':
1788                                 case ':':
1789                                 case '~':
1790                                 case '[':
1791                                 case ']':
1792                                 case '{':
1793                                 case '}':
1794                                 case '(':
1795                                 case ')':
1796                                         // these are all single
1797                                         $match = array($input[0]);
1798                                         $tt = $input[0];
1799                                 break;
1800
1801                                 case '@':
1802                                         throw $this->newSyntaxError('Illegal token');
1803                                 break;
1804
1805                                 case "\n":
1806                                         if ($this->scanNewlines)
1807                                         {
1808                                                 $match = array("\n");
1809                                                 $tt = TOKEN_NEWLINE;
1810                                         }
1811                                         else
1812                                                 throw $this->newSyntaxError('Illegal token');
1813                                 break;
1814
1815                                 default:
1816                                         // FIXME: add support for unicode and unicode escape sequence \uHHHH
1817                                         if (preg_match('/^[$\w]+/', $input, $match))
1818                                         {
1819                                                 $tt = in_array($match[0], $this->keywords) ? $match[0] : TOKEN_IDENTIFIER;
1820                                         }
1821                                         else
1822                                                 throw $this->newSyntaxError('Illegal token');
1823                         }
1824                 }
1825
1826                 $this->tokenIndex = ($this->tokenIndex + 1) & 3;
1827
1828                 if (!isset($this->tokens[$this->tokenIndex]))
1829                         $this->tokens[$this->tokenIndex] = new JSToken();
1830
1831                 $token = $this->tokens[$this->tokenIndex];
1832                 $token->type = $tt;
1833
1834                 if ($tt == OP_ASSIGN)
1835                         $token->assignOp = $op;
1836
1837                 $token->start = $this->cursor;
1838
1839                 $token->value = $match[0];
1840                 $this->cursor += strlen($match[0]);
1841
1842                 $token->end = $this->cursor;
1843                 $token->lineno = $this->lineno;
1844
1845                 return $tt;
1846         }
1847
1848         public function unget()
1849         {
1850                 if (++$this->lookahead == 4)
1851                         throw $this->newSyntaxError('PANIC: too much lookahead!');
1852
1853                 $this->tokenIndex = ($this->tokenIndex - 1) & 3;
1854         }
1855
1856         public function newSyntaxError($m)
1857         {
1858                 return new Exception('Parse error: ' . $m . ' in file \'' . $this->filename . '\' on line ' . $this->lineno);
1859         }
1860 }
1861
1862 class JSToken
1863 {
1864         public $type;
1865         public $value;
1866         public $start;
1867         public $end;
1868         public $lineno;
1869         public $assignOp;
1870 }
1871
1872 ?>