42ec2c78b6febb03820d5969cf1274f7ca537728
[org.ibex.core.git] / src / org / ibex / js / Lexer.java
1 // Derived from org.mozilla.javascript.TokenStream [NPL]
2
3 /**
4  * The contents of this file are subject to the Netscape Public
5  * License Version 1.1 (the "License"); you may not use this file
6  * except in compliance with the License. You may obtain a copy of
7  * the License at http://www.mozilla.org/NPL/
8  *
9  * Software distributed under the License is distributed on an "AS
10  * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
11  * implied. See the License for the specific language governing
12  * rights and limitations under the License.
13  *
14  * The Initial Developer of the Original Code is Netscape
15  * Communications Corporation.
16  *
17  * Contributor(s): Roger Lawrence, Mike McCabe
18  */
19
20 package org.ibex.js;
21 import java.io.*;
22
23 /** Lexes a stream of characters into a stream of Tokens */
24 class Lexer implements Tokens {
25
26     /** for debugging */
27     public static void main(String[] s) throws IOException {
28         Lexer l = new Lexer(new InputStreamReader(System.in), "stdin", 0);
29         int tok = 0;
30         while((tok = l.getToken()) != -1) System.out.println(codeToString[tok]);
31     }
32
33     /** the token that was just parsed */
34     protected int op;
35  
36    /** the most recently parsed token, <i>regardless of pushbacks</i> */
37     protected int mostRecentlyReadToken;
38
39     /** if the token just parsed was a NUMBER, this is the numeric value */
40     protected Number number = null;
41
42     /** if the token just parsed was a NAME or STRING, this is the string value */
43     protected String string = null;
44
45     /** the line number of the most recently <i>lexed</i> token */
46     protected int line = 0;
47
48     /** the line number of the most recently <i>parsed</i> token */
49     protected int parserLine = 0;
50
51     /** the column number of the current token */
52     protected int col = 0;
53
54     /** the name of the source code file being lexed */
55     protected String sourceName;
56
57     private SmartReader in;
58     public Lexer(Reader r, String sourceName, int line) throws IOException {
59         this.sourceName = sourceName;
60         this.line = line;
61         this.parserLine = line;
62         in = new SmartReader(r);
63     }
64
65
66     // Predicates ///////////////////////////////////////////////////////////////////////
67
68     private static boolean isAlpha(int c) { return ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')); }
69     private static boolean isDigit(int c) { return (c >= '0' && c <= '9'); }
70     private static int xDigitToInt(int c) {
71         if ('0' <= c && c <= '9') return c - '0';
72         else if ('a' <= c && c <= 'f') return c - ('a' - 10);
73         else if ('A' <= c && c <= 'F') return c - ('A' - 10);
74         else return -1;
75     }
76
77     
78     // Token Subtype Handlers /////////////////////////////////////////////////////////
79
80     private int getKeyword(String name) throws IOException {
81         //#switch(name)
82         case "if": return IF;
83         case "lt": return LT;
84         case "gt": return GT;
85         case "in": return IN;
86         case "do": return DO;
87         case "and": return AND;
88         case "or": return OR;
89         case "for": return FOR;
90         case "int": return RESERVED;
91         case "new": return RESERVED;
92         case "try": return TRY;
93         case "var": return VAR;
94         case "byte": return RESERVED;
95         case "case": return CASE;
96         case "char": return RESERVED;
97         case "else": return ELSE;
98         case "enum": return RESERVED;
99         case "goto": return RESERVED;
100         case "long": return RESERVED;
101         case "null": return NULL;
102         case "true": return TRUE;
103         case "with": return RESERVED;
104         case "void": return RESERVED;
105         case "class": return RESERVED;
106         case "break": return BREAK;
107         case "while": return WHILE;
108         case "false": return FALSE;
109         case "const": return RESERVED;
110         case "final": return RESERVED;
111         case "super": return RESERVED;
112         case "throw": return THROW;
113         case "catch": return CATCH;
114         case "class": return RESERVED;
115         case "delete": return RESERVED;
116         case "return": return RETURN;
117         case "throws": return RESERVED;
118         case "double": return RESERVED;
119         case "assert": return ASSERT;
120         case "public": return RESERVED;
121         case "switch": return SWITCH;
122         case "typeof": return TYPEOF;
123         case "package": return RESERVED;
124         case "default": return DEFAULT;
125         case "finally": return FINALLY;
126         case "boolean": return RESERVED;
127         case "private": return RESERVED;
128         case "extends": return RESERVED;
129         case "abstract": return RESERVED;
130         case "continue": return CONTINUE;
131         case "debugger": return RESERVED;
132         case "function": return FUNCTION;
133         case "volatile": return RESERVED;
134         case "interface": return RESERVED;
135         case "protected": return RESERVED;
136         case "transient": return RESERVED;
137         case "implements": return RESERVED;
138         case "instanceof": return RESERVED;
139         case "synchronized": return RESERVED;
140         case "cascade": return CASCADE;
141         //#end
142         return -1;
143     }
144
145     private int getIdentifier(int c) throws IOException {
146         in.startString();
147         while (Character.isJavaIdentifierPart((char)(c = in.read())));
148         in.unread();
149         String str = in.getString();
150         int result = getKeyword(str);
151         if (result == RESERVED) throw new LexerException("The reserved word \"" + str + "\" is not permitted in Ibex scripts");
152         if (result != -1) return result;
153         this.string = str.intern();
154         return NAME;
155     }
156     
157     private int getNumber(int c) throws IOException {
158         int base = 10;
159         in.startString();
160         double dval = Double.NaN;
161         long longval = 0;
162         boolean isInteger = true;
163         
164         // figure out what base we're using
165         if (c == '0') {
166             if (Character.toLowerCase((char)(c = in.read())) == 'x') { base = 16; in.startString(); }
167             else if (isDigit(c)) base = 8;
168         }
169         
170         while (0 <= xDigitToInt(c) && !(base < 16 && isAlpha(c))) c = in.read();
171         if (base == 10 && (c == '.' || c == 'e' || c == 'E')) {
172             isInteger = false;
173             if (c == '.') do { c = in.read(); } while (isDigit(c));
174             if (c == 'e' || c == 'E') {
175                 c = in.read();
176                 if (c == '+' || c == '-') c = in.read();
177                 if (!isDigit(c)) throw new LexerException("float listeral did not have an exponent value");
178                 do { c = in.read(); } while (isDigit(c));
179             }
180         }
181         in.unread();
182
183         String numString = in.getString();
184         if (base == 10 && !isInteger) {
185             try { dval = (Double.valueOf(numString)).doubleValue(); }
186             catch (NumberFormatException ex) { throw new LexerException("invalid numeric literal: \"" + numString + "\""); }
187         } else {
188             if (isInteger) {
189                 longval = Long.parseLong(numString, base);
190                 dval = (double)longval;
191             } else {
192                 dval = Double.parseDouble(numString);
193                 longval = (long) dval;
194                 if (longval == dval) isInteger = true;
195             }
196         }
197         
198         if (!isInteger) this.number = new Double(dval);
199         else if(longval >= Integer.MIN_VALUE && longval <= Integer.MAX_VALUE) this.number = new Integer((int)longval);
200         else this.number = new Long(longval);
201         return NUMBER;
202     }
203     
204     private int getString(int c) throws IOException {
205         StringBuffer stringBuf = null;
206         int quoteChar = c;
207         c = in.read();
208         in.startString(); // start after the first "
209         while(c != quoteChar) {
210             if (c == '\n' || c == -1) throw new LexerException("unterminated string literal");
211             if (c == '\\') {
212                 if (stringBuf == null) {
213                     in.unread();   // Don't include the backslash
214                     stringBuf = new StringBuffer(in.getString());
215                     in.read();
216                 }
217                 switch (c = in.read()) {
218                 case 'b': c = '\b'; break;
219                 case 'f': c = '\f'; break;
220                 case 'n': c = '\n'; break;
221                 case 'r': c = '\r'; break;
222                 case 't': c = '\t'; break;
223                 case 'v': c = '\u000B'; break;
224                 case '\\': c = '\\'; break;
225                 case 'u': {
226                     int v = 0;
227                     for(int i=0; i<4; i++) {
228                         int ci = in.read();
229                         if (!((ci >= '0' && ci <= '9') || (ci >= 'a' && ci <= 'f') || (ci >= 'A' && ci <= 'F')))
230                             throw new LexerException("illegal character '" + ((char)c) + "' in \\u unicode escape sequence");
231                         v = (v << 8) | Integer.parseInt(ci + "", 16);
232                     }
233                     c = (char)v;
234                     break;
235                 }
236                 default:
237                     // just use the character that was escaped
238                     break;
239                 }
240             }
241             if (stringBuf != null) stringBuf.append((char) c);
242             c = in.read();
243         }
244         if (stringBuf != null) this.string = stringBuf.toString().intern();
245         else {
246             in.unread(); // miss the trailing "
247             this.string = in.getString().intern();
248             in.read();
249         }
250         return STRING;
251     }
252
253     private int _getToken() throws IOException {
254         int c;
255         do { c = in.read(); } while (c == '\u0020' || c == '\u0009' || c == '\u000C' || c == '\u000B' || c == '\n' );
256         if (c == -1) return -1;
257         if (c == '\\' || Character.isJavaIdentifierStart((char)c)) return getIdentifier(c);
258         if (isDigit(c) || (c == '.' && isDigit(in.peek()))) return getNumber(c);
259         if (c == '"' || c == '\'') return getString(c);
260         switch (c) {
261         case ';': return SEMI;
262         case '[': return LB;
263         case ']': return RB;
264         case '{': return LC;
265         case '}': return RC;
266         case '(': return LP;
267         case ')': return RP;
268         case ',': return COMMA;
269         case '?': return HOOK;
270         case ':': return !in.match(':') ? COLON : in.match('=') ? GRAMMAR : le(":: is not a valid token");
271         case '.': return DOT;
272         case '|': return in.match('|') ? OR : (in.match('=') ? ASSIGN_BITOR : BITOR);
273         case '^': return in.match('=') ? ASSIGN_BITXOR : BITXOR;
274         case '&': return in.match('&') ? AND : in.match('=') ? ASSIGN_BITAND : BITAND;
275         case '=': return !in.match('=') ? ASSIGN : in.match('=') ? SHEQ : EQ;
276         case '!': return !in.match('=') ? BANG : in.match('=') ? SHNE : NE;
277         case '%': return in.match('=') ? ASSIGN_MOD : MOD;
278         case '~': return BITNOT;
279         case '+': return in.match('=') ? ASSIGN_ADD : in.match('+') ? (in.match('=') ? ADD_TRAP : INC) : ADD;
280         case '-': return in.match('=') ? ASSIGN_SUB: in.match('-') ? (in.match('=') ? DEL_TRAP : DEC) : SUB;
281         case '*': return in.match('=') ? ASSIGN_MUL : MUL;
282         case '<': return !in.match('<') ? (in.match('=') ? LE : LT) : in.match('=') ? ASSIGN_LSH : LSH;
283         case '>': return !in.match('>') ? (in.match('=') ? GE : GT) :
284             in.match('>') ? (in.match('=') ? ASSIGN_URSH : URSH) : (in.match('=') ? ASSIGN_RSH : RSH);
285         case '/':
286             if (in.match('=')) return ASSIGN_DIV;
287             if (in.match('/')) { while ((c = in.read()) != -1 && c != '\n'); in.unread(); return getToken(); }
288             if (!in.match('*')) return DIV;
289             while ((c = in.read()) != -1 && !(c == '*' && in.match('/'))) {
290                 if (c == '\n' || c != '/' || !in.match('*')) continue;
291                 if (in.match('/')) return getToken();
292                 throw new LexerException("nested comments are not permitted");
293             }
294             if (c == -1) throw new LexerException("unterminated comment");
295             return getToken();  // `goto retry'
296         default: throw new LexerException("illegal character: \'" + ((char)c) + "\'");
297         }
298     }
299
300     private int le(String s) throws LexerException { if (true) throw new LexerException(s); return 0; }
301
302     // SmartReader ////////////////////////////////////////////////////////////////
303
304     /** a Reader that tracks line numbers and can push back tokens */
305     private class SmartReader {
306         PushbackReader reader = null;
307         int lastread = -1;
308
309         public SmartReader(Reader r) { reader = new PushbackReader(r); }
310         public void unread() throws IOException { unread((char)lastread); }
311         public void unread(char c) throws IOException {
312             reader.unread(c);
313             if(c == '\n') col = -1;
314             else col--;
315             if (accumulator != null) accumulator.setLength(accumulator.length() - 1);
316         }
317         public boolean match(char c) throws IOException { if (peek() == c) { reader.read(); return true; } else return false; }
318         public int peek() throws IOException {
319             int peeked = reader.read();
320             if (peeked != -1) reader.unread((char)peeked);
321             return peeked;
322         }
323         public int read() throws IOException {
324             lastread = reader.read();
325             if (accumulator != null) accumulator.append((char)lastread);
326             if (lastread != '\n' && lastread != '\r') col++;
327             if (lastread == '\n') {
328                 // col is -1 if we just unread a newline, this is sort of ugly
329                 if (col != -1) parserLine = ++line;
330                 col = 0;
331             }
332             return lastread;
333         }
334
335         // FEATURE: could be much more efficient
336         StringBuffer accumulator = null;
337         public void startString() {
338             accumulator = new StringBuffer();
339             accumulator.append((char)lastread);
340         }
341         public String getString() throws IOException {
342             String ret = accumulator.toString().intern();
343             accumulator = null;
344             return ret;
345         }
346     }
347
348
349     // Token PushBack code ////////////////////////////////////////////////////////////
350
351     private int pushBackDepth = 0;
352     private int[] pushBackInts = new int[10];
353     private Object[] pushBackObjects = new Object[10];
354
355     /** push back a token */
356     public final void pushBackToken(int op, Object obj) {
357         if (pushBackDepth >= pushBackInts.length - 1) {
358             int[] newInts = new int[pushBackInts.length * 2];
359             System.arraycopy(pushBackInts, 0, newInts, 0, pushBackInts.length);
360             pushBackInts = newInts;
361             Object[] newObjects = new Object[pushBackObjects.length * 2];
362             System.arraycopy(pushBackObjects, 0, newObjects, 0, pushBackObjects.length);
363             pushBackObjects = newObjects;
364         }
365         pushBackInts[pushBackDepth] = op;
366         pushBackObjects[pushBackDepth] = obj;
367         pushBackDepth++;
368     }
369
370     /** push back the most recently read token */
371     public final void pushBackToken() { pushBackToken(op, number != null ? (Object)number : (Object)string); }
372
373     /** read a token but leave it in the stream */
374     public final int peekToken() throws IOException {
375         int ret = getToken();
376         pushBackToken();
377         return ret;
378     }
379
380     /** read a token */
381     public final int getToken() throws IOException {
382         number = null;
383         string = null;
384         if (pushBackDepth == 0) {
385             mostRecentlyReadToken = op;
386             return op = _getToken();
387         }
388         pushBackDepth--;
389         op = pushBackInts[pushBackDepth];
390         if (pushBackObjects[pushBackDepth] != null) {
391             number = pushBackObjects[pushBackDepth] instanceof Number ? (Number)pushBackObjects[pushBackDepth] : null;
392             string = pushBackObjects[pushBackDepth] instanceof String ? (String)pushBackObjects[pushBackDepth] : null;
393         }
394         return op;
395     }
396
397     class LexerException extends IOException {
398         public LexerException(String s) { super(sourceName + ":" + line + "," + col + ": " + s); }
399     }
400 }