package com.oracle.truffle.js.builtins.helper;
import static com.oracle.js.parser.TokenType.STRING;
import static com.oracle.truffle.js.runtime.builtins.JSAbstractArray.arrayGetArrayType;
import static com.oracle.truffle.js.runtime.builtins.JSAbstractArray.arraySetArrayType;
import com.oracle.js.parser.ECMAErrors;
import com.oracle.js.parser.JSErrorType;
import com.oracle.js.parser.JSType;
import com.oracle.js.parser.ParserException;
import com.oracle.js.parser.Source;
import com.oracle.js.parser.Token;
import com.oracle.truffle.api.object.DynamicObject;
import com.oracle.truffle.js.runtime.JSContext;
import com.oracle.truffle.js.runtime.array.ScriptArray;
import com.oracle.truffle.js.runtime.builtins.JSArray;
import com.oracle.truffle.js.runtime.builtins.JSOrdinary;
import com.oracle.truffle.js.runtime.objects.JSAttributes;
import com.oracle.truffle.js.runtime.objects.JSObjectUtil;
import com.oracle.truffle.js.runtime.objects.Null;
public class NashornJSONParser {
private final String source;
private final JSContext context;
private final int length;
private int pos = 0;
private static final int EOF = -1;
private static final String TRUE = "true";
private static final String FALSE = "false";
private static final String NULL = "null";
private static final int STATE_EMPTY = 0;
private static final int STATE_ELEMENT_PARSED = 1;
private static final int STATE_COMMA_PARSED = 2;
public NashornJSONParser(final String source, final JSContext context) {
this.source = source;
this.context = context;
this.length = source.length();
}
public Object parse() {
final Object value = parseLiteral();
skipWhiteSpace();
if (pos < length) {
throw expectedError(pos, "eof", toString(peek()));
}
return value;
}
private Object parseLiteral() {
skipWhiteSpace();
final int c = peek();
if (c == EOF) {
throw expectedError(pos, "json literal", "eof");
}
switch (c) {
case '{':
return parseObject();
case '[':
return parseArray();
case '"':
return parseString();
case 'f':
return parseKeyword(FALSE, Boolean.FALSE);
case 't':
return parseKeyword(TRUE, Boolean.TRUE);
case 'n':
return parseKeyword(NULL, Null.instance);
default:
if (isDigit(c) || c == '-') {
return parseNumber();
} else if (c == '.') {
throw numberError(pos);
} else {
throw expectedError(pos, "json literal", toString(c));
}
}
}
private Object parseObject() {
DynamicObject jsobject = JSOrdinary.create(context);
int state = STATE_EMPTY;
assert peek() == '{';
pos++;
while (pos < length) {
skipWhiteSpace();
final int c = peek();
switch (c) {
case '"':
if (state == STATE_ELEMENT_PARSED) {
throw expectedError(pos, ", or }", toString(c));
}
final String id = parseString();
expectColon();
final Object value = parseLiteral();
addObjectProperty(jsobject, id, value);
state = STATE_ELEMENT_PARSED;
break;
case ',':
if (state != STATE_ELEMENT_PARSED) {
throw trailingCommaError(pos, toString(c));
}
state = STATE_COMMA_PARSED;
pos++;
break;
case '}':
if (state == STATE_COMMA_PARSED) {
throw trailingCommaError(pos, toString(c));
}
pos++;
return jsobject;
default:
throw expectedError(pos, ", or }", toString(c));
}
}
throw expectedError(pos, ", or }", "eof");
}
private void addObjectProperty(final DynamicObject object, final String id, final Object value) {
JSObjectUtil.defineDataProperty(context, object, id, value, JSAttributes.getDefault());
}
private void expectColon() {
skipWhiteSpace();
final int n = next();
if (n != ':') {
throw expectedError(pos - 1, ":", toString(n));
}
}
private Object parseArray() {
DynamicObject jsarray = JSArray.createEmptyZeroLength(context);
ScriptArray arrayData = arrayGetArrayType(jsarray);
int state = STATE_EMPTY;
assert peek() == '[';
pos++;
while (pos < length) {
skipWhiteSpace();
final int c = peek();
switch (c) {
case ',':
if (state != STATE_ELEMENT_PARSED) {
throw trailingCommaError(pos, toString(c));
}
state = STATE_COMMA_PARSED;
pos++;
break;
case ']':
if (state == STATE_COMMA_PARSED) {
throw trailingCommaError(pos, toString(c));
}
pos++;
return jsarray;
default:
if (state == STATE_ELEMENT_PARSED) {
throw expectedError(pos, ", or ]", toString(c));
}
final long index = arrayData.length(jsarray);
arrayData = arrayData.setElement(jsarray, index, parseLiteral(), true);
arraySetArrayType(jsarray, arrayData);
state = STATE_ELEMENT_PARSED;
break;
}
}
throw expectedError(pos, ", or ]", "eof");
}
private String parseString() {
int start = ++pos;
StringBuilder sb = null;
while (pos < length) {
final int c = next();
if (c <= 0x1f) {
throw syntaxError(pos, "String contains control character");
} else if (c == '\\') {
if (sb == null) {
sb = new StringBuilder(pos - start + 16);
}
sb.append(source, start, pos - 1);
sb.append(parseEscapeSequence());
start = pos;
} else if (c == '"') {
if (sb != null) {
sb.append(source, start, pos - 1);
return sb.toString();
}
return source.substring(start, pos - 1);
}
}
throw error(lexerMessage("missing.close.quote"), pos, length);
}
private char parseEscapeSequence() {
final int c = next();
switch (c) {
case '"':
return '"';
case '\\':
return '\\';
case '/':
return '/';
case 'b':
return '\b';
case 'f':
return '\f';
case 'n':
return '\n';
case 'r':
return '\r';
case 't':
return '\t';
case 'u':
return parseUnicodeEscape();
default:
throw error(lexerMessage("invalid.escape.char"), pos - 1, length);
}
}
private char parseUnicodeEscape() {
return (char) (parseHexDigit() << 12 | parseHexDigit() << 8 | parseHexDigit() << 4 | parseHexDigit());
}
private int parseHexDigit() {
final int c = next();
if (c >= '0' && c <= '9') {
return c - '0';
} else if (c >= 'A' && c <= 'F') {
return c + 10 - 'A';
} else if (c >= 'a' && c <= 'f') {
return c + 10 - 'a';
}
throw error(lexerMessage("invalid.hex"), pos - 1, length);
}
private static boolean isDigit(final int c) {
return c >= '0' && c <= '9';
}
private void skipDigits() {
while (pos < length) {
final int c = peek();
if (!isDigit(c)) {
break;
}
pos++;
}
}
private Number parseNumber() {
final int start = pos;
int c = next();
if (c == '-') {
c = next();
}
if (!isDigit(c)) {
throw numberError(start);
}
if (c != '0') {
skipDigits();
}
if (peek() == '.') {
pos++;
if (!isDigit(next())) {
throw numberError(pos - 1);
}
skipDigits();
}
c = peek();
if (c == 'e' || c == 'E') {
pos++;
c = next();
if (c == '-' || c == '+') {
c = next();
}
if (!isDigit(c)) {
throw numberError(pos - 1);
}
skipDigits();
}
final double d = Double.parseDouble(source.substring(start, pos));
if (JSType.isRepresentableAsInt(d)) {
return (int) d;
} else if (JSType.isRepresentableAsLong(d)) {
return (long) d;
}
return d;
}
private Object parseKeyword(final String keyword, final Object value) {
if (!source.regionMatches(pos, keyword, 0, keyword.length())) {
throw expectedError(pos, "json literal", "ident");
}
pos += keyword.length();
return value;
}
private int peek() {
if (pos >= length) {
return -1;
}
return source.charAt(pos);
}
private int next() {
final int next = peek();
pos++;
return next;
}
private void skipWhiteSpace() {
while (pos < length) {
switch (peek()) {
case '\t':
case '\r':
case '\n':
case ' ':
pos++;
break;
default:
return;
}
}
}
private static String toString(final int c) {
return c == EOF ? "eof" : String.valueOf((char) c);
}
@SuppressWarnings("hiding")
ParserException error(final String message, final int start, final int length) throws ParserException {
final long token = Token.toDesc(STRING, start, length);
final int pos = Token.descPosition(token);
final Source src = Source.sourceFor("<json>", source);
final int lineNum = src.getLine(pos);
final int columnNum = src.getColumn(pos);
return new ParserException(JSErrorType.SyntaxError, message, src, lineNum, columnNum, token);
}
private ParserException error(final String message, final int start) {
return error(message, start, length);
}
private ParserException numberError(final int start) {
return error(lexerMessage("json.invalid.number"), start);
}
private ParserException expectedError(final int start, final String expected, final String found) {
return context.isOptionNashornCompatibilityMode()
? error(parserMessage("expected", expected, found), start)
: expectedErrorV8(start, found);
}
private static ParserException expectedErrorV8(final int start, final String found) {
char c = found.charAt(0);
String entity;
if (c == '"') {
entity = "string";
} else if (Character.isDigit(c)) {
entity = "number";
} else {
entity = String.format("token %s", found);
}
String message = String.format("Unexpected %s in JSON at position %d", entity, start);
return new ParserException(message);
}
private ParserException syntaxError(final int start, final String reason) {
final String message = ECMAErrors.getMessage("syntax.error.invalid.json", reason);
return error(message, start);
}
private static String lexerMessage(final String msgId, String... args) {
return ECMAErrors.getMessage("lexer.error." + msgId, args);
}
private static String parserMessage(final String msgId, String... args) {
return ECMAErrors.getMessage("parser.error." + msgId, args);
}
private ParserException trailingCommaError(int start, String found) {
return context.isOptionNashornCompatibilityMode()
? error(parserMessage("trailing.comma.in.json"), start)
: expectedErrorV8(start, found);
}
}