Compare commits

...

3 Commits

Author SHA1 Message Date
4ed2d5fab5 wip 2020-03-25 08:12:08 +01:00
c9cf30e79f wip 2020-03-21 13:49:44 +01:00
7cc0130ad4 wip expressions 2020-03-21 13:49:44 +01:00
31 changed files with 5103 additions and 5 deletions

View File

@ -0,0 +1,11 @@
export 'src/builtin/builtin.dart';
export 'src/array.dart';
export 'src/arguments.dart';
export 'src/console.dart';
export 'src/context.dart';
export 'src/function.dart';
export 'src/literal.dart';
export 'src/object.dart';
export 'src/interpreter.dart';
export 'src/stack.dart';
export 'src/util.dart';

View File

@ -0,0 +1,28 @@
import 'literal.dart';
import 'object.dart';
class JsArguments extends JsObject {
@override
final List<JsObject> valueOf;
final JsObject callee;
JsArguments(this.valueOf, this.callee) {
properties['callee'] = properties['caller'] = callee;
properties['length'] = JsNumber(valueOf.length);
for (var i = 0; i < valueOf.length; i++) {
properties[i.toDouble()] = valueOf[i];
}
}
@override
String toString() {
if (valueOf.isEmpty) {
return '';
} else if (valueOf.length == 1) {
return valueOf[0].toString();
} else {
return valueOf.map((x) => x.toString()).join(',');
}
}
}

View File

@ -0,0 +1,64 @@
import 'context.dart';
import 'literal.dart';
import 'object.dart';
import 'interpreter.dart';
import 'util.dart';
class JsArray extends JsObject {
@override
final List<JsObject> valueOf = <JsObject>[];
JsArray() {
typeof = 'array';
}
// TODO: Set index???
// TODO: Value of
@override
String toString() {
if (valueOf.isEmpty) {
return '';
} else if (valueOf.length == 1) {
return valueOf[0].toString();
} else {
return valueOf.map((x) => x?.toString() ?? 'undefined').join(',');
}
}
@override
JsObject getProperty(
dynamic name, Interpreter interpreter, InterpreterContext ctx) {
if (name is num) {
// TODO: RangeError?
var v = valueOf[name.toInt()];
return v is JsEmptyItem ? null : v;
} else {
return super.getProperty(name, interpreter, ctx);
}
}
@override
bool removeProperty(
dynamic name, Interpreter interpreter, InterpreterContext ctx) {
if (name is String) {
return removeProperty(
coerceToNumber(JsString(name), interpreter, ctx), interpreter, ctx);
} else if (name is num && name.isFinite) {
var i = name.toInt();
if (i >= 0 && i < valueOf.length) {
valueOf[i] = JsEmptyItem();
}
return true;
} else {
return super.removeProperty(name, interpreter, ctx);
}
}
// TODO: Set property for index..?
}
class JsEmptyItem extends JsObject {
@override
String toString() => '';
}

View File

@ -0,0 +1,44 @@
import '../../interpreter.dart';
class JsBooleanConstructor extends JsConstructor {
static JsBooleanConstructor singleton;
factory JsBooleanConstructor(JsObject context) =>
singleton ??= JsBooleanConstructor._(context);
JsBooleanConstructor._(JsObject context) : super(context, constructor) {
name = 'Boolean';
prototype.addAll(<String, JsObject>{
'constructor': this,
'toString':
wrapFunction(JsBooleanConstructor._toString, context, 'toString'),
'valueOf':
wrapFunction(JsBooleanConstructor._valueOf, context, 'valueOf'),
});
}
static JsObject constructor(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
var first = arguments.getProperty(0.0, interpreter, ctx);
if (first == null) {
return JsBoolean(false);
} else {
return JsBoolean(first.isTruthy);
}
}
static JsObject _toString(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
var v = ctx.scope.context;
return coerceToBoolean(v, (b) {
//print('WTF: ${b.valueOf} from ${v?.properties} (${v}) and ${arguments.valueOf}');
return JsString(b.valueOf.toString());
});
}
static JsObject _valueOf(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
return coerceToBoolean(ctx.scope.context, (b) => b);
}
}

View File

@ -0,0 +1,4 @@
export 'boolean_constructor.dart';
export 'functions.dart';
export 'function_constructor.dart';
export 'misc.dart';

View File

@ -0,0 +1,83 @@
import '../../../parsejs/parsejs.dart';
import '../../interpreter.dart';
class JsFunctionConstructor extends JsConstructor {
static JsFunctionConstructor singleton;
factory JsFunctionConstructor(JsObject context) =>
singleton ??= JsFunctionConstructor._(context);
JsFunctionConstructor._(JsObject context) : super(context, constructor) {
name = 'Function';
prototype.addAll(<String, JsObject>{
'constructor': this,
'apply': wrapFunction(JsFunctionConstructor.apply, context, 'apply'),
'bind': wrapFunction(JsFunctionConstructor._bind, context, 'bind'),
'call': wrapFunction(JsFunctionConstructor._call, context, 'call'),
});
}
static JsObject constructor(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
List<String> paramNames;
Program body;
if (arguments.valueOf.isEmpty) {
paramNames = <String>[];
} else {
paramNames = arguments.valueOf.length <= 1
? <String>[]
: arguments.valueOf
.take(arguments.valueOf.length - 1)
.map((o) => coerceToString(o, interpreter, ctx))
.toList();
body = parsejs(arguments.valueOf.isEmpty
? ''
: coerceToString(arguments.valueOf.last, interpreter, ctx));
}
var f = JsFunction(ctx.scope.context, (interpreter, arguments, ctx) {
ctx = ctx.createChild();
for (var i = 0; i < paramNames.length; i++) {
ctx.scope.create(
paramNames[i],
value: arguments.getProperty(i.toDouble(), interpreter, ctx),
);
}
return body == null
? null
: interpreter.visitProgram(body, 'anonymous function', ctx);
});
f.closureScope = interpreter.globalScope.createChild()
..context = interpreter
.global; // Yes, this is the intended semantics. Operates in the global scope.
return f;
}
static JsObject apply(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
return coerceToFunction(arguments.getProperty(0.0, interpreter, ctx), (f) {
var a1 = arguments.getProperty(1.0, interpreter, ctx);
var args = a1 is JsArray ? a1.valueOf : <JsObject>[];
return interpreter.invoke(f, args, ctx);
});
}
static JsObject _bind(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
return coerceToFunction(
arguments.getProperty(0.0, interpreter, ctx),
(f) => f.bind(
arguments.getProperty(1.0, interpreter, ctx) ?? ctx.scope.context));
}
static JsObject _call(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
return coerceToFunction(arguments.getProperty(0.0, interpreter, ctx), (f) {
return interpreter.invoke(f, arguments.valueOf.skip(1).toList(), ctx);
});
}
}

View File

@ -0,0 +1,13 @@
import '../../interpreter.dart';
import 'boolean_constructor.dart';
import 'function_constructor.dart';
import 'misc.dart';
void loadBuiltinObjects(Interpreter interpreter) {
loadMiscObjects(interpreter);
interpreter.global.properties.addAll(<String, JsObject>{
'Boolean': JsBooleanConstructor(interpreter.global),
'Function': JsFunctionConstructor(interpreter.global),
});
}

View File

@ -0,0 +1,109 @@
import '../../../parsejs/parsejs.dart';
import '../../interpreter.dart';
void loadMiscObjects(Interpreter interpreter) {
var global = interpreter.global;
var decodeUriFunction = JsFunction(global, (interpreter, arguments, ctx) {
try {
return JsString(Uri.decodeFull(
arguments.getProperty(0.0, interpreter, ctx)?.toString()));
} catch (_) {
return arguments.getProperty(0.0, interpreter, ctx);
}
});
var decodeUriComponentFunction =
JsFunction(global, (interpreter, arguments, ctx) {
try {
return JsString(Uri.decodeComponent(
arguments.getProperty(0.0, interpreter, ctx)?.toString()));
} catch (_) {
return arguments.getProperty(0.0, interpreter, ctx);
}
});
var encodeUriFunction = JsFunction(global, (interpreter, arguments, ctx) {
try {
return JsString(Uri.encodeFull(
arguments.getProperty(0.0, interpreter, ctx)?.toString()));
} catch (_) {
return arguments.getProperty(0.0, interpreter, ctx);
}
});
var encodeUriComponentFunction =
JsFunction(global, (interpreter, arguments, ctx) {
try {
return JsString(Uri.encodeComponent(
arguments.getProperty(0.0, interpreter, ctx)?.toString()));
} catch (_) {
return arguments.getProperty(0.0, interpreter, ctx);
}
});
var evalFunction = JsFunction(global, (interpreter, arguments, ctx) {
var src = arguments.getProperty(0.0, interpreter, ctx)?.toString();
if (src == null || src.trim().isEmpty) return null;
try {
var program = parsejs(src, filename: 'eval');
return interpreter.visitProgram(program, 'eval');
} on ParseError catch (e) {
throw ctx.callStack.error('Syntax', e.message);
}
});
var isFinite = JsFunction(global, (interpreter, arguments, ctx) {
return JsBoolean(coerceToNumber(
arguments.getProperty(0.0, interpreter, ctx), interpreter, ctx)
.isFinite);
});
var isNaN = JsFunction(global, (interpreter, arguments, ctx) {
return JsBoolean(coerceToNumber(
arguments.getProperty(0.0, interpreter, ctx), interpreter, ctx)
.isNaN);
});
var parseFloatFunction = JsFunction(global, (interpreter, arguments, ctx) {
var str = arguments.getProperty(0.0, interpreter, ctx)?.toString();
var v = str == null ? null : double.tryParse(str);
return v == null ? null : JsNumber(v);
});
var parseIntFunction = JsFunction(global, (interpreter, arguments, ctx) {
var str = arguments.getProperty(0.0, interpreter, ctx)?.toString();
var baseArg = arguments.getProperty(1.0, interpreter, ctx);
var base = baseArg == null ? 10 : int.tryParse(baseArg.toString());
if (base == null) return JsNumber(double.nan);
var v = str == null
? null
: int.tryParse(str.replaceAll(RegExp(r'^0x'), ''), radix: base);
return v == null ? JsNumber(double.nan) : JsNumber(v);
});
var printFunction = JsFunction(
global,
(interpreter, arguments, scope) {
arguments.valueOf.forEach(print);
return JsNull();
},
);
global.properties.addAll(<String, JsObject>{
'decodeURI': decodeUriFunction..name = 'decodeURI',
'decodeURIComponent': decodeUriComponentFunction
..name = 'decodeURIComponent',
'encodeURI': encodeUriFunction..name = 'encodeURI',
'encodeURIComponent': encodeUriComponentFunction
..name = 'encodeURIComponent',
'eval': evalFunction..name = 'eval',
'Infinity': JsNumber(double.infinity),
'isFinite': isFinite..name = 'isFinite',
'isNaN': isNaN..name = 'isNaN',
'NaN': JsNumber(double.nan),
'parseFloat': parseFloatFunction..name = 'parseFloat',
'parseInt': parseIntFunction..name = 'parseInt',
'print': printFunction..properties['name'] = JsString('print'),
});
}

View File

@ -0,0 +1,150 @@
import 'package:logging/logging.dart';
import 'package:lottie/src/expression/interpreter/interpreter.dart';
import 'arguments.dart';
import 'context.dart';
import 'function.dart';
import 'object.dart';
import 'interpreter.dart';
class JsConsole extends JsObject {
final Map<String, int> _counts = {};
final Map<String, Stopwatch> _time = <String, Stopwatch>{};
final Logger _logger;
JsConsole(this._logger) {
void _func(String name,
JsObject Function(Interpreter, JsArguments, InterpreterContext) f) {
properties[name] = JsFunction(this, f)..name = name;
}
_func('assert', _assert);
_func('clear', _fake('clear'));
_func('count', count);
_func('dir', dir);
_func('dirxml', dirxml);
_func('error', error);
_func('group', _fake('group'));
_func('groupCollapsed', _fake('groupCollapsed'));
_func('groupEnd', _fake('groupEnd'));
_func('info', info);
_func('log', info);
_func('table', table);
_func('time', time);
_func('timeEnd', timeEnd);
_func('trace', trace);
_func('warn', warn);
}
JsObject _assert(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
var condition = arguments.getProperty(0.0, interpreter, ctx);
if (condition?.isTruthy == true) {
_logger.info(arguments.valueOf.skip(1).join(' '));
}
return null;
}
JsObject Function(Interpreter, JsArguments, InterpreterContext ctx) _fake(
String name) {
return (Interpreter interpreter, JsArguments arguments,
InterpreterContext ctx) {
_logger.fine('`console.$name` was called.');
return null;
};
}
JsObject count(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
var label = arguments.getProperty(0.0, interpreter, ctx)?.toString() ??
'<no label>';
_counts.putIfAbsent(label, () => 1);
var v = _counts[label]++;
_logger.info('$label: $v');
return null;
}
JsObject dir(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
var obj = arguments.getProperty(0.0, interpreter, ctx);
if (obj != null) {
_logger.info(obj.properties);
}
return null;
}
JsObject dirxml(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
var obj = arguments.getProperty(0.0, interpreter, ctx);
if (obj != null) {
_logger.info('XML: $obj');
}
return null;
}
JsObject error(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
_logger.severe(arguments.valueOf.join(' '));
return null;
}
JsObject info(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
_logger.info(arguments.valueOf.join(' '));
return null;
}
JsObject warn(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
_logger.warning(arguments.valueOf.join(' '));
return null;
}
JsObject table(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
// TODO: Is there a need to actually make this a table?
_logger.info(arguments.valueOf.join(' '));
return null;
}
JsObject time(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
var label = arguments.getProperty(0.0, interpreter, ctx)?.toString();
if (label != null) {
_time.putIfAbsent(label, () => Stopwatch()..start());
}
return null;
}
JsObject timeEnd(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
var label = arguments.getProperty(0.0, interpreter, ctx)?.toString();
if (label != null) {
var sw = _time.remove(label);
if (sw != null) {
sw.stop();
_logger.info('$label: ${sw.elapsedMicroseconds / 1000}ms');
}
}
return null;
}
JsObject trace(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx) {
for (var frame in ctx.callStack.frames) {
_logger.info(frame);
}
return null;
}
}

View File

@ -0,0 +1,18 @@
import '../../symbol_table/symbol_table.dart';
import 'object.dart';
import 'stack.dart';
class InterpreterContext {
final SymbolTable<JsObject> scope;
final CallStack callStack;
InterpreterContext(this.scope, this.callStack);
InterpreterContext createChild() {
return InterpreterContext(scope.createChild(), callStack.duplicate());
}
InterpreterContext bind(JsObject context) {
return createChild()..scope.context = context;
}
}

View File

@ -0,0 +1,98 @@
import '../../parsejs/parsejs.dart';
import '../interpreter.dart';
import '../../symbol_table/symbol_table.dart';
/// The Dart function that is responsible for the logic of a given [JsFunction].
typedef JsFunctionCallback = JsObject Function(
Interpreter interpreter, JsArguments arguments, InterpreterContext ctx);
class JsFunction extends JsObject {
final JsObject Function(Interpreter, JsArguments, InterpreterContext) f;
final JsObject context;
SymbolTable<JsObject> closureScope;
Node declaration;
JsFunction(this.context, this.f) {
typeof = 'function';
properties['length'] = JsNumber(0);
properties['name'] = JsString('anonymous');
properties['prototype'] = JsObject();
}
bool get isAnonymous {
return properties['name'] == null ||
properties['name'].toString() == 'anonymous';
}
String get name {
if (isAnonymous) {
return '(anonymous function)';
} else {
return properties['name'].toString();
}
}
set name(String value) => properties['name'] = JsString(value);
@override
JsObject getProperty(
dynamic name, Interpreter interpreter, InterpreterContext ctx) {
if (name is JsString) {
return getProperty(name.valueOf, interpreter, ctx);
} else if (name == 'apply') {
return wrapFunction((interpreter, arguments, ctx) {
var a1 = arguments.getProperty(1.0, interpreter, ctx);
var args = a1 is JsArray ? a1.valueOf : <JsObject>[];
return interpreter.invoke(this, args,
arguments.valueOf.isEmpty ? ctx : ctx.bind(arguments.valueOf[0]));
}, this, 'call');
} else if (name == 'bind') {
return wrapFunction(
(_, arguments, ctx) => bind(
arguments.getProperty(0.0, interpreter, ctx) ??
ctx.scope.context),
this,
'bind');
} else if (name == 'call') {
return wrapFunction((interpreter, arguments, ctx) {
var thisCtx = arguments.getProperty(0.0, interpreter, ctx) ??
((arguments.valueOf.isNotEmpty ? arguments.valueOf[0] : null) ??
ctx.scope.context);
return interpreter.invoke(bind(thisCtx),
arguments.valueOf.skip(1).toList(), ctx.bind(thisCtx));
}, this, 'call');
} else if (name == 'constructor') {
return JsFunctionConstructor.singleton;
} else {
return super.getProperty(name, interpreter, ctx);
}
}
JsFunction bind(JsObject newContext) {
if (newContext == null) return bind(JsNull());
var ff = JsFunction(newContext, f)
..properties.addAll(properties)
..closureScope = closureScope?.fork()
..declaration = declaration;
if (isAnonymous || name == null) {
ff.name = 'bound ';
} else {
ff.name = 'bound $name';
}
return ff;
}
@override
String toString() {
return isAnonymous ? '[Function]' : '[Function: $name]';
}
}
class JsConstructor extends JsFunction {
JsConstructor(JsObject context,
JsObject Function(Interpreter, JsArguments, InterpreterContext) f)
: super(context, f);
}

View File

@ -0,0 +1,478 @@
import 'dart:async';
import '../../parsejs/parsejs.dart';
import '../interpreter.dart';
import '../../symbol_table/symbol_table.dart';
class Interpreter {
final List<Completer> awaiting = <Completer>[];
final SymbolTable<JsObject> globalScope = SymbolTable();
final JsObject global = JsObject();
Interpreter() {
globalScope
..context = global
..create('global', value: global);
loadBuiltinObjects(this);
}
JsObject visitProgram(Program node,
[String stackName, InterpreterContext ctx]) {
CallStack callStack;
stackName = node.filename ?? '<entry>';
if (ctx != null) {
callStack = ctx.callStack;
} else {
callStack = CallStack();
ctx = InterpreterContext(globalScope, callStack);
}
callStack.push(node.filename, node.line, stackName);
// TODO: Hoist functions, declarations into global scope.
JsObject out;
for (var stmt in node.body) {
callStack.push(stmt.filename, stmt.line, stackName);
var result = visitStatement(stmt, ctx, stackName);
if (stmt is ExpressionStatement) {
out = result;
}
callStack.pop();
}
callStack.pop();
return out;
}
JsObject visitStatement(
Statement node, InterpreterContext ctx, String stackName) {
var scope = ctx.scope;
var callStack = ctx.callStack;
if (node is ExpressionStatement) {
return visitExpression(node.expression, ctx);
}
if (node is ReturnStatement) {
return visitExpression(node.argument, ctx);
}
if (node is BlockStatement) {
for (var stmt in node.body) {
callStack.push(stmt.filename, stmt.line, stackName);
var result = visitStatement(stmt, ctx.createChild(), stackName);
if (stmt is ReturnStatement) {
callStack.pop();
return result;
}
}
callStack.pop();
return null;
}
if (node is VariableDeclaration) {
for (var decl in node.declarations) {
Variable<JsObject> symbol;
JsObject value;
if (decl.init != null) {
value = visitExpression(decl.init, ctx);
}
try {
symbol = scope.create(decl.name.value, value: value);
} on StateError {
symbol = scope.assign(decl.name.value, value);
}
if (value is JsFunction && value.isAnonymous && symbol != null) {
value.properties['name'] = JsString(symbol.name);
}
}
return null;
}
if (node is FunctionDeclaration) {
return visitFunctionNode(node.function, ctx);
}
if (node is IfStatement) {
var result = visitExpression(node.condition, ctx);
if (result.isTruthy) {
return visitStatement(node.then, ctx, stackName);
} else if (node.otherwise != null) {
return visitStatement(node.otherwise, ctx, stackName);
} else {
return JsNull();
}
}
throw callStack.error('Unsupported', node.runtimeType.toString());
}
JsObject visitExpression(Expression node, InterpreterContext ctx) {
var scope = ctx.scope;
var callStack = ctx.callStack;
if (node is NameExpression) {
if (node.name.value == 'undefined') {
return null;
}
var symbol = scope.resolve(node.name.value);
if (symbol != null) {
return symbol.value;
}
if (global.properties.containsKey(node.name.value)) {
return global.properties[node.name.value];
}
throw callStack.error('Reference', '${node.name.value} is not defined.');
}
if (node is MemberExpression) {
var target = visitExpression(node.object, ctx);
return target?.getProperty(node.property.value, this, ctx);
}
if (node is ThisExpression) {
return scope.context;
}
if (node is ObjectExpression) {
var props = <dynamic, JsObject>{};
for (var prop in node.properties) {
props[prop.nameString] = visitExpression(prop.expression, ctx);
}
return JsObject()..properties.addAll(props);
}
if (node is LiteralExpression) {
if (node.isBool) {
return JsBoolean(node.boolValue);
} else if (node.isString) {
return JsString(node.stringValue);
} else if (node.isNumber) {
return JsNumber(node.numberValue);
} else if (node.isNull) {
return JsNull();
}
}
if (node is ConditionalExpression) {
var condition = visitExpression(node.condition, ctx);
return (condition?.isTruthy == true)
? visitExpression(node.then, ctx)
: visitExpression(node.otherwise, ctx);
}
if (node is IndexExpression) {
// TODO: What are the actual semantics of this in JavaScript?
var target = visitExpression(node.object, ctx);
var index = visitExpression(node.property, ctx);
return target.properties[index.valueOf];
}
if (node is CallExpression) {
var target = visitExpression(node.callee, ctx);
if (target is JsFunction) {
var arguments = JsArguments(
node.arguments.map((e) => visitExpression(e, ctx)).toList(),
target);
var childScope = target.closureScope ?? scope;
childScope = childScope.createChild(values: {'arguments': arguments});
childScope.context = target.context ?? scope.context;
JsObject result;
if (target.declaration != null) {
callStack.push(target.declaration.filename, target.declaration.line,
target.name);
}
if (node.isNew && target is! JsConstructor) {
result = target.newInstance();
childScope.context = result;
target.f(this, arguments, InterpreterContext(childScope, callStack));
} else {
result = target.f(
this, arguments, InterpreterContext(childScope, callStack));
}
if (target.declaration != null) {
callStack.pop();
}
return result;
} else {
if (node.isNew) {
throw callStack.error('Type',
'${target?.valueOf ?? 'undefined'} is not a constructor.');
} else {
throw callStack.error(
'Type', '${target?.valueOf ?? 'undefined'} is not a function.');
}
}
}
if (node is FunctionExpression) {
return visitFunctionNode(node.function, ctx);
}
if (node is ArrayExpression) {
var items = node.expressions.map((e) => visitExpression(e, ctx));
return JsArray()..valueOf.addAll(items);
}
if (node is BinaryExpression) {
var left = visitExpression(node.left, ctx);
var right = visitExpression(node.right, ctx);
return performBinaryOperation(node.operator, left, right, ctx);
}
if (node is AssignmentExpression) {
var l = node.left;
if (l is NameExpression) {
if (node.operator == '=') {
return scope
.assign(l.name.value, visitExpression(node.right, ctx))
.value;
} else {
var trimmedOp = node.operator.substring(0, node.operator.length - 1);
return scope
.assign(
l.name.value,
performNumericalBinaryOperation(
trimmedOp,
visitExpression(l, ctx),
visitExpression(node.right, ctx),
ctx,
),
)
.value;
}
} else if (l is MemberExpression) {
var left = visitExpression(l.object, ctx);
if (node.operator == '=') {
return left.setProperty(
l.property.value, visitExpression(node.right, ctx));
} else {
var trimmedOp = node.operator.substring(0, node.operator.length - 1);
return left.setProperty(
l.property.value,
performNumericalBinaryOperation(
trimmedOp,
left.getProperty(l.property.value, this, ctx),
visitExpression(node.right, ctx),
ctx,
),
);
}
} else if (l is IndexExpression) {
// TODO: Set values, extend arrays
} else {
throw callStack.error(
'Reference', 'Invalid left-hand side in assignment');
}
}
if (node is SequenceExpression) {
return node.expressions.map((e) => visitExpression(e, ctx)).last;
}
if (node is UnaryExpression) {
if (node.operator == 'delete') {
var left = node.argument;
if (left is IndexExpression) {
var l = visitExpression(left.object, ctx);
var property = visitExpression(left.property, ctx);
var idx = coerceToNumber(property, this, ctx);
if (l is JsArray && idx.isFinite) {
if (idx >= 0 && idx < l.valueOf.length) {
l.valueOf[idx.toInt()] = JsEmptyItem();
}
} else if (l is! JsBuiltinObject) {
return JsBoolean(l.removeProperty(property, this, ctx));
}
} else if (left is MemberExpression) {
var l = visitExpression(left.object, ctx);
if (l is! JsBuiltinObject) {
return JsBoolean(l.removeProperty(left.property.value, this, ctx));
}
}
return JsBoolean(true);
}
var expr = visitExpression(node.argument, ctx);
// +, -, !, ~, typeof, void, delete
switch (node.operator) {
case '!':
return JsBoolean(expr?.isTruthy != true);
case '+':
return JsNumber(coerceToNumber(expr, this, ctx));
case '~':
var n = coerceToNumber(expr, this, ctx);
if (!n.isFinite) return JsNumber(n);
return JsNumber(-(n + 1));
case '-':
var value = coerceToNumber(expr, this, ctx);
if (value == null || value.isNaN) {
return JsNumber(double.nan);
} else if (!value.isFinite) {
return JsNumber(
value.isNegative ? double.infinity : double.negativeInfinity);
} else {
return JsNumber(-1.0 * value);
}
break;
case 'typeof':
return JsString(expr?.typeof ?? 'undefined');
case 'void':
return null;
default:
throw callStack.error('Unsupported', node.operator);
}
}
throw callStack.error('Unsupported', node.runtimeType.toString());
}
JsObject performBinaryOperation(
String op, JsObject left, JsObject right, InterpreterContext ctx) {
// TODO: May be: ==, !=, ===, !==, in, instanceof
if (op == '==') {
return JsBoolean(left == right);
// TODO: Loose equality
throw UnimplementedError('== operator');
} else if (op == '===') {
// TODO: Override operator
return JsBoolean(left == right);
} else if (op == '&&') {
return (left?.isTruthy != true) ? left : right;
} else if (op == '||') {
return (left?.isTruthy == true) ? left : right;
} else if (op == '<') {
return safeBooleanOperation(left, right, this, ctx, (l, r) => l < r);
} else if (op == '<=') {
return safeBooleanOperation(left, right, this, ctx, (l, r) => l <= r);
} else if (op == '>') {
return safeBooleanOperation(left, right, this, ctx, (l, r) => l > r);
} else if (op == '>=') {
return safeBooleanOperation(left, right, this, ctx, (l, r) => l >= r);
} else {
return performNumericalBinaryOperation(op, left, right, ctx);
}
}
JsObject performNumericalBinaryOperation(
String op, JsObject left, JsObject right, InterpreterContext ctx) {
if (op == '+' && (!canCoerceToNumber(left) || !canCoerceToNumber(right))) {
return JsString(left.toString() + right.toString());
} else {
var l = coerceToNumber(left, this, ctx);
var r = coerceToNumber(right, this, ctx);
if (l.isNaN || r.isNaN) {
return JsNumber(double.nan);
}
// =, +=, -=, *=, /=, %=, <<=, >>=, >>>=, |=, ^=, &=
switch (op) {
case '+':
return JsNumber(l + r);
case '-':
return JsNumber(l - r);
case '*':
return JsNumber(l * r);
case '/':
return JsNumber(l / r);
case '%':
return JsNumber(l % r);
case '<<':
return JsNumber(l.toInt() << r.toInt());
case '>>':
return JsNumber(l.toInt() >> r.toInt());
case '>>>':
// TODO: Is a zero-filled right shift relevant with Dart?
return JsNumber(l.toInt() >> r.toInt());
case '|':
return JsNumber(l.toInt() | r.toInt());
case '^':
return JsNumber(l.toInt() ^ r.toInt());
case '&':
return JsNumber(l.toInt() & r.toInt());
default:
throw ArgumentError();
}
}
}
JsObject visitFunctionNode(FunctionNode node, InterpreterContext ctx) {
JsFunction function;
function = JsFunction(ctx.scope.context, (interpreter, arguments, ctx) {
for (var i = 0.0; i < node.params.length; i++) {
ctx.scope.create(node.params[i.toInt()].value,
value: arguments.properties[i]);
}
return visitStatement(node.body, ctx, function.name);
});
function.declaration = node;
function.properties['length'] = JsNumber(node.params.length);
function.properties['name'] = JsString(node.name?.value ?? 'anonymous');
// TODO: What about hoisting???
if (node.name != null) {
ctx.scope.create(node.name.value, value: function, constant: true);
}
function.closureScope = ctx.scope.fork();
function.closureScope.context = ctx.scope.context;
return function;
}
JsObject invoke(
JsFunction target, List<JsObject> args, InterpreterContext ctx) {
var scope = ctx.scope, callStack = ctx.callStack;
var childScope = target.closureScope ?? scope;
var arguments = JsArguments(args, target);
childScope = childScope.createChild(values: {'arguments': arguments});
childScope.context = target.context ?? scope.context;
print('${target.context} => ${childScope.context}');
JsObject result;
if (target.declaration != null) {
callStack.push(
target.declaration.filename, target.declaration.line, target.name);
}
result =
target.f(this, arguments, InterpreterContext(childScope, callStack));
if (target.declaration != null) {
callStack.pop();
}
return result;
}
}

View File

@ -0,0 +1,71 @@
import 'object.dart';
class JsBoolean extends JsObject {
@override
final bool valueOf;
JsBoolean(this.valueOf) {
typeof = 'boolean';
}
@override
bool get isTruthy => valueOf;
@override
String toString() => valueOf.toString();
}
// TODO: Prototype???
class JsString extends JsObject {
@override
final String valueOf;
JsString(this.valueOf) {
typeof = 'string';
}
@override
bool get isTruthy => valueOf.isNotEmpty;
@override
String toString() => valueOf;
}
// TODO: Prototype???
class JsNumber extends JsObject {
final num _valueOf;
JsNumber(this._valueOf) {
typeof = 'number';
}
@override
double get valueOf => _valueOf.toDouble();
@override
bool get isTruthy => valueOf != 0.0;
@override
String toString() {
if (valueOf.isNaN) {
return 'NaN';
} else if (valueOf.isInfinite) {
return valueOf.isNegative ? '-Infinity' : 'Infinity';
} else {
return (valueOf == valueOf.toInt())
? valueOf.toInt().toString()
: valueOf.toString();
}
}
}
class JsNull extends JsObject {
@override
Null get valueOf => null;
@override
bool get isTruthy => false;
@override
String toString() => 'null';
}

View File

@ -0,0 +1,101 @@
import 'context.dart';
import 'function.dart';
import 'literal.dart';
import 'interpreter.dart';
import 'util.dart';
class JsObject {
final properties = <dynamic, JsObject>{};
//final Map<String, JsObject> prototype = {};
String typeof = 'object';
bool get isTruthy => true;
dynamic get valueOf => properties;
// ignore: avoid_returning_this
JsObject get jsValueOf => this;
bool isLooselyEqualTo(JsObject other) {
// TODO: Finish this stupidity
return false;
}
dynamic coerceIndex(dynamic name) {
if (name is JsNumber) {
return name.toString();
} else if (name is JsString) {
return name.toString();
} else {
return name;
}
}
JsObject getProperty(
dynamic name, Interpreter interpreter, InterpreterContext ctx) {
name = coerceIndex(name);
if (name == 'valueOf') {
return properties['valueOf'] ??
wrapFunction(
(_, __, _____) => jsValueOf, ctx.scope.context, 'valueOf');
}
// if (name == 'prototype') {
// return new JsPrototype(prototype);
// } else {
return properties[name];
// }
}
bool removeProperty(
dynamic name, Interpreter interpreter, InterpreterContext ctx) {
name = coerceIndex(name);
properties.remove(name);
return true;
/*
if (name is JsObject) {
return removeProperty(coerceToString(name, interpreter, ctx), interpreter, ctx);
} else if (name is String) {
} else {
properties.remove(name);
return true;
}
*/
}
Map<dynamic, JsObject> get prototype {
return (properties['prototype'] ??= JsObject()).properties;
}
JsObject newInstance() {
var obj = JsObject();
var p = prototype;
for (var key in p.keys) {
var value = p[key];
if (value is JsFunction) {
obj.properties[key] = value.bind(obj);
} else {
obj.properties[key] = value;
}
}
return obj;
}
@override
String toString() => '[object Object]';
JsObject setProperty(dynamic name, JsObject value) {
return properties[coerceIndex(name)] = value;
}
}
class JsBuiltinObject extends JsObject {}
class JsPrototype extends JsObject {
JsPrototype();
}

View File

@ -0,0 +1,59 @@
import 'dart:collection';
class CallStack {
final Queue<Frame> _frames = Queue<Frame>();
List<Frame> get frames => _frames.toList();
void clear() => _frames.clear();
void push(String filename, int line, String name) {
_frames.addFirst(Frame(filename, line, name));
}
void pop() {
if (_frames.isNotEmpty) _frames.removeFirst();
}
InterpreterException error(String type, String message) {
var msg = '${type}Error: $message';
var frames = _frames.toList();
return InterpreterException(msg, frames);
}
CallStack duplicate() {
return CallStack().._frames.addAll(_frames);
}
}
class Frame {
final String filename;
final int line;
final String name;
Frame(this.filename, this.line, this.name);
@override
String toString() =>
filename == null ? '$name:$line' : '$name:$filename:$line';
}
class InterpreterException implements Exception {
final String message;
final List<Frame> stackTrace;
InterpreterException(this.message, this.stackTrace);
@override
String toString() {
var b = StringBuffer('$message');
if (stackTrace.isNotEmpty) {
b.writeln();
b.writeln();
stackTrace.forEach(b.writeln);
}
return b.toString();
}
}

View File

@ -0,0 +1,90 @@
import 'array.dart';
import 'context.dart';
import 'function.dart';
import 'literal.dart';
import 'object.dart';
import 'interpreter.dart';
bool canCoerceToNumber(JsObject object) {
return object is JsNumber ||
object is JsBoolean ||
object is JsNull ||
object == null ||
(object is JsArray && object.valueOf.length != 1) ||
object.properties.containsKey('valueOf');
}
double coerceToNumber(
JsObject object, Interpreter interpreter, InterpreterContext ctx) {
if (object is JsNumber) {
return object.valueOf;
} else if (object == null) {
return double.nan;
} else if (object is JsNull) {
return 0.0;
} else if (object is JsBoolean) {
return object.valueOf ? 1.0 : 0.0;
} else if (object is JsArray && object.valueOf.isEmpty) {
return 0.0;
} else if (object is JsString) {
return num.tryParse(object.valueOf)?.toDouble() ?? double.nan;
} else {
var valueOfFunc = object?.getProperty('valueOf', interpreter, ctx);
if (valueOfFunc != null) {
if (valueOfFunc is JsFunction) {
return coerceToNumber(
interpreter.invoke(valueOfFunc, [], ctx), interpreter, ctx);
}
return double.nan;
} else {
return double.nan;
}
}
}
String coerceToString(
JsObject object, Interpreter interpreter, InterpreterContext ctx) {
if (object == null) {
return 'undefined';
} else {
return object.toString();
}
}
JsObject coerceToFunction(JsObject obj, JsObject Function(JsFunction) f) {
if (obj is! JsFunction) {
return null;
} else {
return f(obj as JsFunction);
}
}
JsObject coerceToBoolean(JsObject obj, JsObject Function(JsBoolean) f) {
if (obj is! JsBoolean) {
return f(JsBoolean(obj?.isTruthy ?? false));
} else {
return f(obj as JsBoolean);
}
}
JsBoolean safeBooleanOperation(
JsObject left,
JsObject right,
Interpreter interpreter,
InterpreterContext ctx,
bool Function(num, num) f) {
var l = coerceToNumber(left, interpreter, ctx);
var r = coerceToNumber(right, interpreter, ctx);
if (l.isNaN || r.isNaN) {
return JsBoolean(false);
} else {
return JsBoolean(f(l, r));
}
}
JsFunction wrapFunction(JsFunctionCallback f, JsObject context, [String name]) {
return JsFunction(context, f)..name = name;
}

View File

@ -0,0 +1,49 @@
library jsparser;
import 'src/ast.dart';
import 'src/lexer.dart';
import 'src/parser.dart';
import 'src/annotations.dart';
import 'src/noise.dart';
export 'src/ast.dart';
export 'src/lexer.dart' show ParseError;
/// Parse [text] as a JavaScript program and return its AST.
///
/// Options:
///
/// - [filename]: a string indicating where the source came from (this string has no special syntax or meaning).
///
/// - [firstLine]: line number to associate with the first line of code.
///
/// - [handleNoise]: tolerate noise, such as hash bangs (#!) and HTML comment markers (<!--,-->) (default: true).
///
/// - [annotations]: if true, [Node.parent], [Scope.environment], and [Name.scope] will be initialized (default: true).
///
/// - [parseAsExpression]: if true, parse the input as an expression statement.
Program parsejs(String text,
{String filename,
int firstLine = 1,
bool handleNoise = true,
bool annotations = true,
bool parseAsExpression = false}) {
var offset = Offsets(0, text.length, firstLine);
if (handleNoise) {
offset = trimNoise(text, offset);
}
var lexer = Lexer(text,
filename: filename,
currentLine: offset.line,
index: offset.start,
endOfFile: offset.end);
var parser = Parser(lexer);
var ast = parseAsExpression
? parser.parseExpressionProgram()
: parser.parseProgram();
if (annotations) {
annotateAST(ast);
}
ast.filename = filename;
return ast;
}

View File

@ -0,0 +1,111 @@
library scope;
import 'ast.dart';
void annotateAST(Program ast) {
setParentPointers(ast);
EnvironmentBuilder()..build(ast);
Resolver()..resolve(ast);
}
void setParentPointers(Node node, [Node parent]) {
node.parent = parent;
node.forEach((child) => setParentPointers(child, node));
}
/// Initializes [Scope.environment] for all scopes in a given AST.
class EnvironmentBuilder extends RecursiveVisitor<void> {
void build(Program ast) {
visit(ast);
}
Scope currentScope; // Program or FunctionExpression
void addVar(Name name) {
currentScope.environment.add(name.value);
}
@override
void visitProgram(Program node) {
node.environment = <String>{};
currentScope = node;
node.forEach(visit);
}
@override
void visitFunctionNode(FunctionNode node) {
node.environment = <String>{};
var oldScope = currentScope;
currentScope = node;
node.environment.add('arguments');
if (node.isExpression && node.name != null) {
addVar(node.name);
}
node.params.forEach(addVar);
visit(node.body);
currentScope = oldScope;
}
@override
void visitFunctionDeclaration(FunctionDeclaration node) {
addVar(node.function.name);
visit(node.function);
}
@override
void visitFunctionExpression(FunctionExpression node) {
visit(node.function);
}
@override
void visitVariableDeclarator(VariableDeclarator node) {
addVar(node.name);
node.forEach(visit);
}
@override
void visitCatchClause(CatchClause node) {
node.environment = <String>{};
node.environment.add(node.param.value);
node.forEach(visit);
}
}
/// Initializes the [Name.scope] link on all [Name] nodes.
class Resolver extends RecursiveVisitor<void> {
void resolve(Program ast) {
visit(ast);
}
Scope enclosingScope(Node node) {
while (node is! Scope) {
node = node.parent;
}
return node as Scope;
}
Scope findScope(Name nameNode) {
var name = nameNode.value;
var parent = nameNode.parent;
Node node = nameNode;
if (parent is FunctionNode && parent.name == node && !parent.isExpression) {
node = parent.parent;
}
var scope = enclosingScope(node);
while (scope is! Program) {
if (scope.environment == null) {
throw Exception('$scope does not have an environment');
}
if (scope.environment.contains(name)) return scope;
scope = enclosingScope(scope.parent);
}
return scope;
}
@override
void visitName(Name node) {
if (node.isVariable) {
node.scope = findScope(node);
}
}
}

View File

@ -0,0 +1,884 @@
library ast;
part 'ast_visitor.dart';
// ignore_for_file: prefer_single_quotes
// ignore_for_file: annotate_overrides
// ignore_for_file: always_declare_return_types
// ignore_for_file: avoid_renaming_method_parameters
// ignore_for_file: omit_local_variable_types
// AST structure mostly designed after the Mozilla Parser API:
// https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API
/// A node in the abstract syntax tree of a JavaScript program.
abstract class Node {
/// The parent of this node, or null if this is the [Program] node.
///
/// If you transform the AST in any way, it is your own responsibility to update parent pointers accordingly.
Node parent;
/// Source-code offset.
int start, end;
/// 1-based line number.
int line;
/// Retrieves the filename from the enclosing [Program]. Returns null if the node is orphaned.
String get filename {
var program = enclosingProgram;
if (program != null) return program.filename;
return null;
}
/// A string with filename and line number.
String get location => "$filename:$line";
/// Returns the [Program] node enclosing this node, possibly the node itself, or null if not enclosed in any program.
Program get enclosingProgram {
var node = this;
while (node != null) {
if (node is Program) return node;
node = node.parent;
}
return null;
}
/// Returns the [FunctionNode] enclosing this node, possibly the node itself, or null if not enclosed in any function.
FunctionNode get enclosingFunction {
var node = this;
while (node != null) {
if (node is FunctionNode) return node;
node = node.parent;
}
return null;
}
/// Visits the immediate children of this node.
void forEach(void Function(Node node) callback);
/// Calls the relevant `visit` method on the visitor.
T visitBy<T>(Visitor<T> visitor);
/// Calls the relevant `visit` method on the visitor.
T visitBy1<T, A>(Visitor1<T, A> visitor, A arg);
}
/// Superclass for [Program], [FunctionNode], and [CatchClause], which are the three types of node that
/// can host local variables.
abstract class Scope extends Node {
/// Variables declared in this scope, including the implicitly declared "arguments" variable.
Set<String> environment;
}
/// A collection of [Program] nodes.
///
/// This node is not generated by the parser, but is a convenient way to cluster multiple ASTs into a single AST,
/// should you wish to do so.
class Programs extends Node {
List<Program> programs = <Program>[];
Programs(this.programs);
void forEach(callback) => programs.forEach(callback);
String toString() => 'Programs';
visitBy<T>(Visitor<T> v) => v.visitPrograms(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitPrograms(this, arg);
}
/// The root node of a JavaScript AST, representing the top-level scope.
class Program extends Scope {
/// Indicates where the program was parsed from.
/// In principle, this can be anything, it is just a string passed to the parser for convenience.
String filename;
List<Statement> body;
Program(this.body);
void forEach(callback) => body.forEach(callback);
String toString() => 'Program';
visitBy<T>(Visitor<T> v) => v.visitProgram(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitProgram(this, arg);
}
/// A function, which may occur as a function expression, function declaration, or property accessor in an object literal.
class FunctionNode extends Scope {
Name name;
List<Name> params;
Statement body;
FunctionNode(this.name, this.params, this.body);
bool get isExpression => parent is FunctionExpression;
bool get isDeclaration => parent is FunctionDeclaration;
bool get isAccessor => parent is Property && (parent as Property).isAccessor;
forEach(callback) {
if (name != null) callback(name);
params.forEach(callback);
callback(body);
}
String toString() => 'FunctionNode';
visitBy<T>(Visitor<T> v) => v.visitFunctionNode(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitFunctionNode(this, arg);
}
/// Mention of a variable, property, or label.
class Name extends Node {
/// Name being referenced.
///
/// Unicode values have been resolved.
String value;
/// Link to the enclosing [FunctionExpression], [Program], or [CatchClause] where this variable is declared
/// (defaults to [Program] if undeclared), or `null` if this is not a variable.
Scope scope;
/// True if this refers to a variable name.
bool get isVariable =>
parent is NameExpression ||
parent is FunctionNode ||
parent is VariableDeclarator ||
parent is CatchClause;
/// True if this refers to a property name.
bool get isProperty =>
(parent is MemberExpression &&
(parent as MemberExpression).property == this) ||
(parent is Property && (parent as Property).key == this);
/// True if this refers to a label name.
bool get isLabel =>
parent is BreakStatement ||
parent is ContinueStatement ||
parent is LabeledStatement;
Name(this.value);
void forEach(callback) {}
String toString() => '$value';
visitBy<T>(Visitor<T> v) => v.visitName(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitName(this, arg);
}
/// Superclass for all nodes that are statements.
abstract class Statement extends Node {}
/// Statement of form: `;`
class EmptyStatement extends Statement {
void forEach(callback) {}
String toString() => 'EmptyStatement';
visitBy<T>(Visitor<T> v) => v.visitEmptyStatement(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitEmptyStatement(this, arg);
}
/// Statement of form: `{ [body] }`
class BlockStatement extends Statement {
List<Statement> body;
BlockStatement(this.body);
void forEach(callback) => body.forEach(callback);
String toString() => 'BlockStatement';
visitBy<T>(Visitor<T> v) => v.visitBlock(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitBlock(this, arg);
}
/// Statement of form: `[expression];`
class ExpressionStatement extends Statement {
Expression expression;
ExpressionStatement(this.expression);
forEach(callback) => callback(expression);
String toString() => 'ExpressionStatement';
visitBy<T>(Visitor<T> v) => v.visitExpressionStatement(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) =>
v.visitExpressionStatement(this, arg);
}
/// Statement of form: `if ([condition]) then [then] else [otherwise]`.
class IfStatement extends Statement {
Expression condition;
Statement then;
Statement otherwise; // May be null.
IfStatement(this.condition, this.then, [this.otherwise]);
forEach(callback) {
callback(condition);
callback(then);
if (otherwise != null) callback(otherwise);
}
String toString() => 'IfStatement';
visitBy<T>(Visitor<T> v) => v.visitIf(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitIf(this, arg);
}
/// Statement of form: `[label]: [body]`
class LabeledStatement extends Statement {
Name label;
Statement body;
LabeledStatement(this.label, this.body);
forEach(callback) {
callback(label);
callback(body);
}
String toString() => 'LabeledStatement';
visitBy<T>(Visitor<T> v) => v.visitLabeledStatement(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitLabeledStatement(this, arg);
}
/// Statement of form: `break;` or `break [label];`
class BreakStatement extends Statement {
Name label; // May be null.
BreakStatement(this.label);
forEach(callback) {
if (label != null) callback(label);
}
String toString() => 'BreakStatement';
visitBy<T>(Visitor<T> v) => v.visitBreak(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitBreak(this, arg);
}
/// Statement of form: `continue;` or `continue [label];`
class ContinueStatement extends Statement {
Name label; // May be null.
ContinueStatement(this.label);
forEach(callback) {
if (label != null) callback(label);
}
String toString() => 'ContinueStatement';
visitBy<T>(Visitor<T> v) => v.visitContinue(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitContinue(this, arg);
}
/// Statement of form: `with ([object]) { [body] }`
class WithStatement extends Statement {
Expression object;
Statement body;
WithStatement(this.object, this.body);
forEach(callback) {
callback(object);
callback(body);
}
String toString() => 'WithStatement';
visitBy<T>(Visitor<T> v) => v.visitWith(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitWith(this, arg);
}
/// Statement of form: `switch ([argument]) { [cases] }`
class SwitchStatement extends Statement {
Expression argument;
List<SwitchCase> cases;
SwitchStatement(this.argument, this.cases);
forEach(callback) {
callback(argument);
cases.forEach(callback);
}
String toString() => 'SwitchStatement';
visitBy<T>(Visitor<T> v) => v.visitSwitch(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitSwitch(this, arg);
}
/// Clause in a switch: `case [expression]: [body]` or `default: [body]` if [expression] is null.
class SwitchCase extends Node {
Expression expression; // May be null (for default clause)
List<Statement> body;
SwitchCase(this.expression, this.body);
SwitchCase.defaultCase(this.body);
/// True if this is a default clause, and not a case clause.
bool get isDefault => expression == null;
forEach(callback) {
if (expression != null) callback(expression);
body.forEach(callback);
}
String toString() => 'SwitchCase';
visitBy<T>(Visitor<T> v) => v.visitSwitchCase(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitSwitchCase(this, arg);
}
/// Statement of form: `return [argument];` or `return;`
class ReturnStatement extends Statement {
Expression argument;
ReturnStatement(this.argument);
forEach(callback) => argument != null ? callback(argument) : null;
String toString() => 'ReturnStatement';
visitBy<T>(Visitor<T> v) => v.visitReturn(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitReturn(this, arg);
}
/// Statement of form: `throw [argument];`
class ThrowStatement extends Statement {
Expression argument;
ThrowStatement(this.argument);
forEach(callback) => callback(argument);
String toString() => 'ThrowStatement';
visitBy<T>(Visitor<T> v) => v.visitThrow(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitThrow(this, arg);
}
/// Statement of form: `try [block] catch [handler] finally [finalizer]`.
class TryStatement extends Statement {
BlockStatement block;
CatchClause handler; // May be null
BlockStatement finalizer; // May be null (but not if handler is null)
TryStatement(this.block, this.handler, this.finalizer);
forEach(callback) {
callback(block);
if (handler != null) callback(handler);
if (finalizer != null) callback(finalizer);
}
String toString() => 'TryStatement';
visitBy<T>(Visitor<T> v) => v.visitTry(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitTry(this, arg);
}
/// A catch clause: `catch ([param]) [body]`
class CatchClause extends Scope {
Name param;
BlockStatement body;
CatchClause(this.param, this.body);
forEach(callback) {
callback(param);
callback(body);
}
String toString() => 'CatchClause';
visitBy<T>(Visitor<T> v) => v.visitCatchClause(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitCatchClause(this, arg);
}
/// Statement of form: `while ([condition]) [body]`
class WhileStatement extends Statement {
Expression condition;
Statement body;
WhileStatement(this.condition, this.body);
forEach(callback) {
callback(condition);
callback(body);
}
String toString() => 'WhileStatement';
visitBy<T>(Visitor<T> v) => v.visitWhile(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitWhile(this, arg);
}
/// Statement of form: `do [body] while ([condition]);`
class DoWhileStatement extends Statement {
Statement body;
Expression condition;
DoWhileStatement(this.body, this.condition);
forEach(callback) {
callback(body);
callback(condition);
}
String toString() => 'DoWhileStatement';
visitBy<T>(Visitor<T> v) => v.visitDoWhile(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitDoWhile(this, arg);
}
/// Statement of form: `for ([init]; [condition]; [update]) [body]`
class ForStatement extends Statement {
/// May be VariableDeclaration, Expression, or null.
Node init;
Expression condition; // May be null.
Expression update; // May be null.
Statement body;
ForStatement(this.init, this.condition, this.update, this.body);
forEach(callback) {
if (init != null) callback(init);
if (condition != null) callback(condition);
if (update != null) callback(update);
callback(body);
}
String toString() => 'ForStatement';
visitBy<T>(Visitor<T> v) => v.visitFor(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitFor(this, arg);
}
/// Statement of form: `for ([left] in [right]) [body]`
class ForInStatement extends Statement {
/// May be VariableDeclaration or Expression.
Node left;
Expression right;
Statement body;
ForInStatement(this.left, this.right, this.body);
forEach(callback) {
callback(left);
callback(right);
callback(body);
}
String toString() => 'ForInStatement';
visitBy<T>(Visitor<T> v) => v.visitForIn(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitForIn(this, arg);
}
/// Statement of form: `function [function.name])([function.params]) { [function.body] }`.
class FunctionDeclaration extends Statement {
FunctionNode function;
FunctionDeclaration(this.function);
forEach(callback) => callback(function);
String toString() => 'FunctionDeclaration';
visitBy<T>(Visitor<T> v) => v.visitFunctionDeclaration(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) =>
v.visitFunctionDeclaration(this, arg);
}
/// Statement of form: `var [declarations];`
class VariableDeclaration extends Statement {
List<VariableDeclarator> declarations;
VariableDeclaration(this.declarations);
forEach(callback) => declarations.forEach(callback);
String toString() => 'VariableDeclaration';
visitBy<T>(Visitor<T> v) => v.visitVariableDeclaration(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) =>
v.visitVariableDeclaration(this, arg);
}
/// Variable declaration: `[name]` or `[name] = [init]`.
class VariableDeclarator extends Node {
Name name;
Expression init; // May be null.
VariableDeclarator(this.name, this.init);
forEach(callback) {
callback(name);
if (init != null) callback(init);
}
String toString() => 'VariableDeclarator';
visitBy<T>(Visitor<T> v) => v.visitVariableDeclarator(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) =>
v.visitVariableDeclarator(this, arg);
}
/// Statement of form: `debugger;`
class DebuggerStatement extends Statement {
forEach(callback) {}
String toString() => 'DebuggerStatement';
visitBy<T>(Visitor<T> v) => v.visitDebugger(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitDebugger(this, arg);
}
///////
/// Superclass of all nodes that are expressions.
abstract class Expression extends Node {}
/// Expression of form: `this`
class ThisExpression extends Expression {
forEach(callback) {}
String toString() => 'ThisExpression';
visitBy<T>(Visitor<T> v) => v.visitThis(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitThis(this, arg);
}
/// Expression of form: `[ [expressions] ]`
class ArrayExpression extends Expression {
List<Expression>
expressions; // May CONTAIN nulls for omitted elements: e.g. [1,2,,,]
ArrayExpression(this.expressions);
forEach(callback) {
for (Expression exp in expressions) {
if (exp != null) {
callback(exp);
}
}
}
String toString() => 'ArrayExpression';
visitBy<T>(Visitor<T> v) => v.visitArray(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitArray(this, arg);
}
/// Expression of form: `{ [properties] }`
class ObjectExpression extends Expression {
List<Property> properties;
ObjectExpression(this.properties);
forEach(callback) => properties.forEach(callback);
String toString() => 'ObjectExpression';
visitBy<T>(Visitor<T> v) => v.visitObject(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitObject(this, arg);
}
/// Property initializer `[key]: [value]`, or getter `get [key] [value]`, or setter `set [key] [value]`.
///
/// For getters and setters, [value] is a [FunctionNode], otherwise it is an [Expression].
class Property extends Node {
/// Literal or Name indicating the name of the property. Use [nameString] to get the name as a string.
Node key;
/// A [FunctionNode] (for getters and setters) or an [Expression] (for ordinary properties).
Node value;
/// May be "init", "get", or "set".
String kind;
Property(this.key, this.value, [this.kind = 'init']);
// Property.getter(this.key, FunctionExpression this.value) : kind = 'get';
// Property.setter(this.key, FunctionExpression this.value) : kind = 'set';
bool get isInit => kind == 'init';
bool get isGetter => kind == 'get';
bool get isSetter => kind == 'set';
bool get isAccessor => isGetter || isSetter;
String get nameString => key is Name
? (key as Name).value
: (key as LiteralExpression).value.toString();
/// Returns the value as a FunctionNode. Useful for getters/setters.
FunctionNode get function => value as FunctionNode;
/// Returns the value as an Expression. Useful for non-getter/setters.
Expression get expression => value as Expression;
forEach(callback) {
callback(key);
callback(value);
}
String toString() => 'Property';
visitBy<T>(Visitor<T> v) => v.visitProperty(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitProperty(this, arg);
}
/// Expression of form: `function [function.name]([function.params]) { [function.body] }`.
class FunctionExpression extends Expression {
FunctionNode function;
FunctionExpression(this.function);
forEach(callback) => callback(function);
String toString() => 'FunctionExpression';
visitBy<T>(Visitor<T> v) => v.visitFunctionExpression(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) =>
v.visitFunctionExpression(this, arg);
}
/// Comma-seperated expressions.
class SequenceExpression extends Expression {
List<Expression> expressions;
SequenceExpression(this.expressions);
forEach(callback) => expressions.forEach(callback);
String toString() => 'SequenceExpression';
visitBy<T>(Visitor<T> v) => v.visitSequence(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitSequence(this, arg);
}
/// Expression of form: `+[argument]`, or using any of the unary operators:
/// `+, -, !, ~, typeof, void, delete`
class UnaryExpression extends Expression {
String operator; // May be: +, -, !, ~, typeof, void, delete
Expression argument;
UnaryExpression(this.operator, this.argument);
forEach(callback) => callback(argument);
String toString() => 'UnaryExpression';
visitBy<T>(Visitor<T> v) => v.visitUnary(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitUnary(this, arg);
}
/// Expression of form: `[left] + [right]`, or using any of the binary operators:
/// `==, !=, ===, !==, <, <=, >, >=, <<, >>, >>>, +, -, *, /, %, |, ^, &, &&, ||, in, instanceof`
class BinaryExpression extends Expression {
Expression left;
String
operator; // May be: ==, !=, ===, !==, <, <=, >, >=, <<, >>, >>>, +, -, *, /, %, |, ^, &, &&, ||, in, instanceof
Expression right;
BinaryExpression(this.left, this.operator, this.right);
forEach(callback) {
callback(left);
callback(right);
}
String toString() => 'BinaryExpression';
visitBy<T>(Visitor<T> v) => v.visitBinary(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitBinary(this, arg);
}
/// Expression of form: `[left] = [right]` or `[left] += [right]` or using any of the assignment operators:
/// `=, +=, -=, *=, /=, %=, <<=, >>=, >>>=, |=, ^=, &=`
class AssignmentExpression extends Expression {
Expression left;
String operator; // May be: =, +=, -=, *=, /=, %=, <<=, >>=, >>>=, |=, ^=, &=
Expression right;
AssignmentExpression(this.left, this.operator, this.right);
bool get isCompound => operator.length > 1;
forEach(callback) {
callback(left);
callback(right);
}
String toString() => 'AssignmentExpression';
visitBy<T>(Visitor<T> v) => v.visitAssignment(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitAssignment(this, arg);
}
/// Expression of form: `++[argument]`, `--[argument]`, `[argument]++`, `[argument]--`.
class UpdateExpression extends Expression {
String operator; // May be: ++, --
Expression argument;
bool isPrefix;
UpdateExpression(this.operator, this.argument, this.isPrefix);
UpdateExpression.prefix(this.operator, this.argument) : isPrefix = true;
UpdateExpression.postfix(this.operator, this.argument) : isPrefix = false;
forEach(callback) => callback(argument);
String toString() => 'UpdateExpression';
visitBy<T>(Visitor<T> v) => v.visitUpdateExpression(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitUpdateExpression(this, arg);
}
/// Expression of form: `[condition] ? [then] : [otherwise]`.
class ConditionalExpression extends Expression {
Expression condition;
Expression then;
Expression otherwise;
ConditionalExpression(this.condition, this.then, this.otherwise);
forEach(callback) {
callback(condition);
callback(then);
callback(otherwise);
}
String toString() => 'ConditionalExpression';
visitBy<T>(Visitor<T> v) => v.visitConditional(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitConditional(this, arg);
}
/// Expression of form: `[callee](..[arguments]..)` or `new [callee](..[arguments]..)`.
class CallExpression extends Expression {
bool isNew;
Expression callee;
List<Expression> arguments;
CallExpression(this.callee, this.arguments, {this.isNew = false});
CallExpression.newCall(this.callee, this.arguments) : isNew = true;
forEach(callback) {
callback(callee);
arguments.forEach(callback);
}
String toString() => 'CallExpression';
visitBy<T>(Visitor<T> v) => v.visitCall(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitCall(this, arg);
}
/// Expression of form: `[object].[property].`
class MemberExpression extends Expression {
Expression object;
Name property;
MemberExpression(this.object, this.property);
forEach(callback) {
callback(object);
callback(property);
}
String toString() => 'MemberExpression';
visitBy<T>(Visitor<T> v) => v.visitMember(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitMember(this, arg);
}
/// Expression of form: `[object][[property]]`.
class IndexExpression extends Expression {
Expression object;
Expression property;
IndexExpression(this.object, this.property);
forEach(callback) {
callback(object);
callback(property);
}
String toString() => 'IndexExpression';
visitBy<T>(Visitor<T> v) => v.visitIndex(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitIndex(this, arg);
}
/// A [Name] that is used as an expression.
///
/// Note that "undefined", "NaN", and "Infinity" are name expressions, and not literals and one might expect.
class NameExpression extends Expression {
Name name;
NameExpression(this.name);
forEach(callback) => callback(name);
String toString() => 'NameExpression';
visitBy<T>(Visitor<T> v) => v.visitNameExpression(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitNameExpression(this, arg);
}
/// A literal string, number, boolean or null.
///
/// Note that "undefined", "NaN", and "Infinity" are [NameExpression]s, and not literals and one might expect.
class LiteralExpression extends Expression {
/// A string, number, boolean, or null value, indicating the value of the literal.
dynamic value;
/// The verbatim source-code representation of the literal.
String raw;
LiteralExpression(this.value, [this.raw]);
bool get isString => value is String;
bool get isNumber => value is num;
bool get isBool => value is bool;
bool get isNull => value == null;
String get stringValue => value as String;
num get numberValue => value as num;
bool get boolValue => value as bool;
/// Converts the value to a string
String get toName => value.toString();
forEach(callback) {}
String toString() => 'LiteralExpression';
visitBy<T>(Visitor<T> v) => v.visitLiteral(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitLiteral(this, arg);
}
/// A regular expression literal.
class RegexpExpression extends Expression {
/// The entire literal, including slashes and flags.
String regexp;
RegexpExpression(this.regexp);
forEach(callback) {}
String toString() => 'RegexpExpression';
visitBy<T>(Visitor<T> v) => v.visitRegexp(this);
visitBy1<T, A>(Visitor1<T, A> v, A arg) => v.visitRegexp(this, arg);
}

View File

@ -0,0 +1,260 @@
part of ast;
// ignore_for_file: prefer_single_quotes
// ignore_for_file: annotate_overrides
// ignore_for_file: always_declare_return_types
// ignore_for_file: avoid_renaming_method_parameters
// ignore_for_file: omit_local_variable_types
/// Visitor interface for AST nodes.
///
/// Also see [BaseVisitor] and [RecursiveVisitor].
abstract class Visitor<T> {
/// Shorthand for `node.visitBy(this)`.
T visit(Node node) => node.visitBy(this);
T visitPrograms(Programs node);
T visitProgram(Program node);
T visitFunctionNode(FunctionNode node);
T visitName(Name node);
T visitEmptyStatement(EmptyStatement node);
T visitBlock(BlockStatement node);
T visitExpressionStatement(ExpressionStatement node);
T visitIf(IfStatement node);
T visitLabeledStatement(LabeledStatement node);
T visitBreak(BreakStatement node);
T visitContinue(ContinueStatement node);
T visitWith(WithStatement node);
T visitSwitch(SwitchStatement node);
T visitSwitchCase(SwitchCase node);
T visitReturn(ReturnStatement node);
T visitThrow(ThrowStatement node);
T visitTry(TryStatement node);
T visitCatchClause(CatchClause node);
T visitWhile(WhileStatement node);
T visitDoWhile(DoWhileStatement node);
T visitFor(ForStatement node);
T visitForIn(ForInStatement node);
T visitFunctionDeclaration(FunctionDeclaration node);
T visitVariableDeclaration(VariableDeclaration node);
T visitVariableDeclarator(VariableDeclarator node);
T visitDebugger(DebuggerStatement node);
T visitThis(ThisExpression node);
T visitArray(ArrayExpression node);
T visitObject(ObjectExpression node);
T visitProperty(Property node);
T visitFunctionExpression(FunctionExpression node);
T visitSequence(SequenceExpression node);
T visitUnary(UnaryExpression node);
T visitBinary(BinaryExpression node);
T visitAssignment(AssignmentExpression node);
T visitUpdateExpression(UpdateExpression node);
T visitConditional(ConditionalExpression node);
T visitCall(CallExpression node);
T visitMember(MemberExpression node);
T visitIndex(IndexExpression node);
T visitNameExpression(NameExpression node);
T visitLiteral(LiteralExpression node);
T visitRegexp(RegexpExpression node);
}
/// Implementation of [Visitor] which redirects each `visit` method to a method [defaultNode].
///
/// This is convenient when only a couple of `visit` methods are needed
/// and a default action can be taken for all other nodes.
class BaseVisitor<T> implements Visitor<T> {
T defaultNode(Node node) => null;
T visit(Node node) => node.visitBy(this);
T visitPrograms(Programs node) => defaultNode(node);
T visitProgram(Program node) => defaultNode(node);
T visitFunctionNode(FunctionNode node) => defaultNode(node);
T visitName(Name node) => defaultNode(node);
T visitEmptyStatement(EmptyStatement node) => defaultNode(node);
T visitBlock(BlockStatement node) => defaultNode(node);
T visitExpressionStatement(ExpressionStatement node) => defaultNode(node);
T visitIf(IfStatement node) => defaultNode(node);
T visitLabeledStatement(LabeledStatement node) => defaultNode(node);
T visitBreak(BreakStatement node) => defaultNode(node);
T visitContinue(ContinueStatement node) => defaultNode(node);
T visitWith(WithStatement node) => defaultNode(node);
T visitSwitch(SwitchStatement node) => defaultNode(node);
T visitSwitchCase(SwitchCase node) => defaultNode(node);
T visitReturn(ReturnStatement node) => defaultNode(node);
T visitThrow(ThrowStatement node) => defaultNode(node);
T visitTry(TryStatement node) => defaultNode(node);
T visitCatchClause(CatchClause node) => defaultNode(node);
T visitWhile(WhileStatement node) => defaultNode(node);
T visitDoWhile(DoWhileStatement node) => defaultNode(node);
T visitFor(ForStatement node) => defaultNode(node);
T visitForIn(ForInStatement node) => defaultNode(node);
T visitFunctionDeclaration(FunctionDeclaration node) => defaultNode(node);
T visitVariableDeclaration(VariableDeclaration node) => defaultNode(node);
T visitVariableDeclarator(VariableDeclarator node) => defaultNode(node);
T visitDebugger(DebuggerStatement node) => defaultNode(node);
T visitThis(ThisExpression node) => defaultNode(node);
T visitArray(ArrayExpression node) => defaultNode(node);
T visitObject(ObjectExpression node) => defaultNode(node);
T visitProperty(Property node) => defaultNode(node);
T visitFunctionExpression(FunctionExpression node) => defaultNode(node);
T visitSequence(SequenceExpression node) => defaultNode(node);
T visitUnary(UnaryExpression node) => defaultNode(node);
T visitBinary(BinaryExpression node) => defaultNode(node);
T visitAssignment(AssignmentExpression node) => defaultNode(node);
T visitUpdateExpression(UpdateExpression node) => defaultNode(node);
T visitConditional(ConditionalExpression node) => defaultNode(node);
T visitCall(CallExpression node) => defaultNode(node);
T visitMember(MemberExpression node) => defaultNode(node);
T visitIndex(IndexExpression node) => defaultNode(node);
T visitNameExpression(NameExpression node) => defaultNode(node);
T visitLiteral(LiteralExpression node) => defaultNode(node);
T visitRegexp(RegexpExpression node) => defaultNode(node);
}
/// Traverses the entire subtree when visiting a node.
///
/// When overriding a `visitXXX` method, it is your responsibility to visit
/// the children of the given node, otherwise that subtree will not be traversed.
///
/// For example:
///
/// visitWhile(While node) {
/// print('Found while loop on line ${node.line}');
/// node.forEach(visit); // visit children
/// }
///
/// Without the call to `forEach`, a while loop nested in another while loop would
/// not be found.
class RecursiveVisitor<T> extends BaseVisitor<T> {
defaultNode(Node node) {
node.forEach(visit);
return null;
}
}
/// Visitor interface that takes an argument.
///
/// If multiple arguments are needed, they must be wrapped in a parameter object.
///
/// That there is no [RecursiveVisitor] variant that takes an argument.
/// For a generic traversal that takes arguments, consider using [BaseVisitor1] and
/// override `defaultNode` to visit the children of each node.
abstract class Visitor1<T, A> {
/// Shorthand for `node.visitBy(this)`.
T visit(Node node, A arg) => node.visitBy1(this, arg);
T visitPrograms(Programs node, A arg);
T visitProgram(Program node, A arg);
T visitFunctionNode(FunctionNode node, A arg);
T visitName(Name node, A arg);
T visitEmptyStatement(EmptyStatement node, A arg);
T visitBlock(BlockStatement node, A arg);
T visitExpressionStatement(ExpressionStatement node, A arg);
T visitIf(IfStatement node, A arg);
T visitLabeledStatement(LabeledStatement node, A arg);
T visitBreak(BreakStatement node, A arg);
T visitContinue(ContinueStatement node, A arg);
T visitWith(WithStatement node, A arg);
T visitSwitch(SwitchStatement node, A arg);
T visitSwitchCase(SwitchCase node, A arg);
T visitReturn(ReturnStatement node, A arg);
T visitThrow(ThrowStatement node, A arg);
T visitTry(TryStatement node, A arg);
T visitCatchClause(CatchClause node, A arg);
T visitWhile(WhileStatement node, A arg);
T visitDoWhile(DoWhileStatement node, A arg);
T visitFor(ForStatement node, A arg);
T visitForIn(ForInStatement node, A arg);
T visitFunctionDeclaration(FunctionDeclaration node, A arg);
T visitVariableDeclaration(VariableDeclaration node, A arg);
T visitVariableDeclarator(VariableDeclarator node, A arg);
T visitDebugger(DebuggerStatement node, A arg);
T visitThis(ThisExpression node, A arg);
T visitArray(ArrayExpression node, A arg);
T visitObject(ObjectExpression node, A arg);
T visitProperty(Property node, A arg);
T visitFunctionExpression(FunctionExpression node, A arg);
T visitSequence(SequenceExpression node, A arg);
T visitUnary(UnaryExpression node, A arg);
T visitBinary(BinaryExpression node, A arg);
T visitAssignment(AssignmentExpression node, A arg);
T visitUpdateExpression(UpdateExpression node, A arg);
T visitConditional(ConditionalExpression node, A arg);
T visitCall(CallExpression node, A arg);
T visitMember(MemberExpression node, A arg);
T visitIndex(IndexExpression node, A arg);
T visitNameExpression(NameExpression node, A arg);
T visitLiteral(LiteralExpression node, A arg);
T visitRegexp(RegexpExpression node, A arg);
}
/// Implementation of [Visitor1] which redirects each `visit` method to a method [defaultNode].
///
/// This is convenient when only a couple of `visit` methods are needed
/// and a default action can be taken for all other nodes.
class BaseVisitor1<T, A> implements Visitor1<T, A> {
T defaultNode(Node node, A arg) => null;
T visit(Node node, A arg) => node.visitBy1(this, arg);
T visitPrograms(Programs node, A arg) => defaultNode(node, arg);
T visitProgram(Program node, A arg) => defaultNode(node, arg);
T visitFunctionNode(FunctionNode node, A arg) => defaultNode(node, arg);
T visitName(Name node, A arg) => defaultNode(node, arg);
T visitEmptyStatement(EmptyStatement node, A arg) => defaultNode(node, arg);
T visitBlock(BlockStatement node, A arg) => defaultNode(node, arg);
T visitExpressionStatement(ExpressionStatement node, A arg) =>
defaultNode(node, arg);
T visitIf(IfStatement node, A arg) => defaultNode(node, arg);
T visitLabeledStatement(LabeledStatement node, A arg) =>
defaultNode(node, arg);
T visitBreak(BreakStatement node, A arg) => defaultNode(node, arg);
T visitContinue(ContinueStatement node, A arg) => defaultNode(node, arg);
T visitWith(WithStatement node, A arg) => defaultNode(node, arg);
T visitSwitch(SwitchStatement node, A arg) => defaultNode(node, arg);
T visitSwitchCase(SwitchCase node, A arg) => defaultNode(node, arg);
T visitReturn(ReturnStatement node, A arg) => defaultNode(node, arg);
T visitThrow(ThrowStatement node, A arg) => defaultNode(node, arg);
T visitTry(TryStatement node, A arg) => defaultNode(node, arg);
T visitCatchClause(CatchClause node, A arg) => defaultNode(node, arg);
T visitWhile(WhileStatement node, A arg) => defaultNode(node, arg);
T visitDoWhile(DoWhileStatement node, A arg) => defaultNode(node, arg);
T visitFor(ForStatement node, A arg) => defaultNode(node, arg);
T visitForIn(ForInStatement node, A arg) => defaultNode(node, arg);
T visitFunctionDeclaration(FunctionDeclaration node, A arg) =>
defaultNode(node, arg);
T visitVariableDeclaration(VariableDeclaration node, A arg) =>
defaultNode(node, arg);
T visitVariableDeclarator(VariableDeclarator node, A arg) =>
defaultNode(node, arg);
T visitDebugger(DebuggerStatement node, A arg) => defaultNode(node, arg);
T visitThis(ThisExpression node, A arg) => defaultNode(node, arg);
T visitArray(ArrayExpression node, A arg) => defaultNode(node, arg);
T visitObject(ObjectExpression node, A arg) => defaultNode(node, arg);
T visitProperty(Property node, A arg) => defaultNode(node, arg);
T visitFunctionExpression(FunctionExpression node, A arg) =>
defaultNode(node, arg);
T visitSequence(SequenceExpression node, A arg) => defaultNode(node, arg);
T visitUnary(UnaryExpression node, A arg) => defaultNode(node, arg);
T visitBinary(BinaryExpression node, A arg) => defaultNode(node, arg);
T visitAssignment(AssignmentExpression node, A arg) => defaultNode(node, arg);
T visitUpdateExpression(UpdateExpression node, A arg) =>
defaultNode(node, arg);
T visitConditional(ConditionalExpression node, A arg) =>
defaultNode(node, arg);
T visitCall(CallExpression node, A arg) => defaultNode(node, arg);
T visitMember(MemberExpression node, A arg) => defaultNode(node, arg);
T visitIndex(IndexExpression node, A arg) => defaultNode(node, arg);
T visitNameExpression(NameExpression node, A arg) => defaultNode(node, arg);
T visitLiteral(LiteralExpression node, A arg) => defaultNode(node, arg);
T visitRegexp(RegexpExpression node, A arg) => defaultNode(node, arg);
}

View File

@ -0,0 +1,115 @@
library charcode;
// ignore_for_file: constant_identifier_names
const int NULL = 0;
const int BS = 8; // backspace
const int TAB = 9;
const int LF = 10; // line feed (\n)
const int VTAB = 11; // vertical tab
const int FF = 12; // form feed (new page)
const int CR = 13; // carriage return (\r)
const int SPACE = 32;
const int BANG = 33; // !
const int DQUOTE = 34; // "
const int HASH = 35; // #
const int DOLLAR = 36;
const int PERCENT = 37;
const int AMPERSAND = 38; // &
const int SQUOTE = 39; // '
const int LPAREN = 40;
const int RPAREN = 41;
const int STAR = 42;
const int PLUS = 43;
const int COMMA = 44;
const int MINUS = 45;
const int DOT = 46;
const int SLASH = 47;
const int $0 = 48;
const int $1 = 49;
const int $2 = 50;
const int $3 = 51;
const int $4 = 52;
const int $5 = 53;
const int $6 = 54;
const int $7 = 55;
const int $8 = 56;
const int $9 = 57;
const int COLON = 58;
const int SEMICOLON = 59;
const int LT = 60; // <
const int EQ = 61; // =
const int GT = 62; // >
const int QUESTION = 63; // ?
const int AT = 64; // @
const int $A = 65;
const int $B = 66;
const int $C = 67;
const int $D = 68;
const int $E = 69;
const int $F = 70;
const int $G = 71;
const int $H = 72;
const int $I = 73;
const int $J = 74;
const int $K = 75;
const int $L = 76;
const int $M = 77;
const int $N = 78;
const int $O = 79;
const int $P = 80;
const int $Q = 81;
const int $R = 82;
const int $S = 83;
const int $T = 84;
const int $U = 85;
const int $V = 86;
const int $W = 87;
const int $X = 88;
const int $Y = 89;
const int $Z = 90;
const int LBRACKET = 91; // [
const int BACKSLASH = 92;
const int RBRACKET = 93; // ]
const int HAT = 94; // ^
const int UNDERSCORE = 95;
const int BACKTICK = 96;
const int $a = 97;
const int $b = 98;
const int $c = 99;
const int $d = 100;
const int $e = 101;
const int $f = 102;
const int $g = 103;
const int $h = 104;
const int $i = 105;
const int $j = 106;
const int $k = 107;
const int $l = 108;
const int $m = 109;
const int $n = 110;
const int $o = 111;
const int $p = 112;
const int $q = 113;
const int $r = 114;
const int $s = 115;
const int $t = 116;
const int $u = 117;
const int $v = 118;
const int $w = 119;
const int $x = 120;
const int $y = 121;
const int $z = 122;
const int LBRACE = 123; // {
const int BAR = 124; // |
const int RBRACE = 125; // }
const int TILDE = 126; // ~
const int ZWNJ = 0x200C; // Zero width non-joiner
const int ZWJ = 0x200D; // Zero width joiner
const int NBSP = 0x00A0; // No-break space
const int BOM = 0xFEFF; // Byte order mark
const int LS = 0x2028; // line separator
const int PS = 0x2029; // paragraph separator

View File

@ -0,0 +1,728 @@
library lexer;
import 'charcode.dart' as char;
// ignore_for_file: prefer_single_quotes
// ignore_for_file: annotate_overrides
// ignore_for_file: always_declare_return_types
// ignore_for_file: avoid_renaming_method_parameters
// ignore_for_file: constant_identifier_names
// ignore_for_file: omit_local_variable_types
class ParseError implements Exception {
String message;
String filename;
int line; // 1-based line number.
int startOffset;
int endOffset;
ParseError(
this.message, this.filename, this.line, this.startOffset, this.endOffset);
String get location => filename == null ? 'Line $line' : '$filename:$line';
String toString() => '[$location] $message';
}
class Token {
int startOffset;
int line;
int type;
String
text; // text exactly as in source code or null for EOF or tokens with type > 31
bool afterLinebreak; // true if first token after a linebreak
String
value; // value of identifier or string literal after escapes, null for other tokens
/// For tokens that can be used as binary operators, this indicates their relative precedence.
/// Set to -100 for other tokens.
/// Token type can be BINARY, or UNARY (+,-) or NAME (instanceof,in).
int binaryPrecedence = -100;
Token(this.startOffset, this.line, this.type, this.afterLinebreak, this.text);
String toString() => text ?? typeToString(type);
String get detailedString => "[$startOffset, $text, $type, $afterLinebreak]";
int get endOffset => startOffset + (text == null ? 1 : text.length);
static const int EOF = 0;
static const int NAME = 1;
static const int NUMBER = 2;
static const int BINARY =
3; // does not contain unary operators +,- or names instanceof, in (but binaryPrecedence is set for these)
static const int ASSIGN = 4; // also compound assignment operators
static const int UPDATE = 5; // ++ and --
static const int UNARY =
6; // all unary operators except the names void, delete
static const int STRING = 7;
static const int REGEXP = 8;
// Tokens without a text have type equal to their corresponding char code
// All these are >31
static const int LPAREN = char.LPAREN;
static const int RPAREN = char.RPAREN;
static const int LBRACE = char.LBRACE;
static const int RBRACE = char.RBRACE;
static const int LBRACKET = char.LBRACKET;
static const int RBRACKET = char.RBRACKET;
static const int COMMA = char.COMMA;
static const int COLON = char.COLON;
static const int SEMICOLON = char.SEMICOLON;
static const int DOT = char.DOT;
static const int QUESTION = char.QUESTION;
static String typeToString(int type) {
if (type > 31) return "'${String.fromCharCode(type)}'";
switch (type) {
case EOF:
return 'EOF';
case NAME:
return 'name';
case NUMBER:
return 'number';
case BINARY:
return 'binary operator';
case ASSIGN:
return 'assignment operator';
case UPDATE:
return 'update operator';
case UNARY:
return 'unary operator';
case STRING:
return 'string literal';
default:
return '[type $type]';
}
}
}
class Precedence {
static const int EXPRESSION = 0;
static const int CONDITIONAL = 1;
static const int LOGICAL_OR = 2;
static const int LOGICAL_AND = 3;
static const int BITWISE_OR = 4;
static const int BITWISE_XOR = 5;
static const int BITWISE_AND = 6;
static const int EQUALITY = 7;
static const int RELATIONAL = 8;
static const int SHIFT = 9;
static const int ADDITIVE = 10;
static const int MULTIPLICATIVE = 11;
}
bool isLetter(int x) =>
(char.$a <= x && x <= char.$z) || (char.$A <= x && x <= char.$Z);
bool isDigit(int x) =>
char.$0 <= x &&
x <= char.$9; // Does NOT and should not include unicode special digits
bool isNameStart(int x) =>
isLetter(x) || x == char.DOLLAR || x == char.UNDERSCORE;
bool isNamePart(int x) =>
char.$a <= x && x <= char.$z ||
char.$A <= x && x <= char.$Z ||
char.$0 <= x && x <= char.$9 ||
x == char.DOLLAR ||
x == char.UNDERSCORE;
bool isFancyNamePart(int x) => x == char.ZWNJ || x == char.ZWJ || x == char.BOM;
/// Ordinary whitespace (not line terminators)
bool isWhitespace(int x) {
switch (x) {
case char.SPACE:
case char.TAB:
case char.VTAB:
case char.FF:
case char.BOM:
return true;
default:
return false;
}
}
bool isEOL(int x) {
switch (x) {
case char.LF:
case char.CR:
case char.LS:
case char.PS:
case char.NULL:
return true;
default:
return false;
}
}
class Lexer {
Lexer(String text,
{this.filename, this.currentLine = 1, this.index = 0, this.endOfFile}) {
input = text.codeUnits;
endOfFile ??= input.length;
}
List<int> input;
int index = 0;
int endOfFile;
int tokenStart;
int tokenLine;
int currentLine; // We use 1-based line numbers.
bool seenLinebreak;
String filename;
int get current => index == endOfFile ? char.NULL : input[index];
int next() {
++index;
return index == endOfFile ? char.NULL : input[index];
}
void fail(String message) {
throw ParseError(message, filename, currentLine, tokenStart, index);
}
Token emitToken(int type, [String value]) {
return Token(tokenStart, tokenLine, type, seenLinebreak, value);
}
Token emitValueToken(int type) {
String value = String.fromCharCodes(input.getRange(tokenStart, index));
return Token(tokenStart, tokenLine, type, seenLinebreak, value);
}
Token scanNumber(int x) {
if (x == char.$0) {
x = next();
if (x == char.$x || x == char.$X) {
x = next();
return scanHexNumber(x);
}
}
while (isDigit(x)) {
x = next();
}
if (x == char.DOT) {
x = next();
return scanDecimalPart(x);
}
return scanExponentPart(x);
}
Token scanDecimalPart(int x) {
while (isDigit(x)) {
x = next();
}
return scanExponentPart(x);
}
Token scanExponentPart(int x) {
if (x == char.$e || x == char.$E) {
x = next();
if (x == char.PLUS || x == char.MINUS) {
x = next();
}
while (isDigit(x)) {
x = next();
}
}
return emitValueToken(Token.NUMBER);
}
Token scanHexNumber(int x) {
while (isDigit(x) ||
char.$a <= x && x <= char.$f ||
char.$A <= x && x <= char.$F) {
x = next();
}
return emitValueToken(Token.NUMBER);
}
Token scanName(int x) {
while (true) {
if (x == char.BACKSLASH) return scanComplexName(x);
if (!isNamePart(x)) {
Token tok = emitValueToken(Token.NAME);
tok.value = tok.text;
return tok..binaryPrecedence = Precedence.RELATIONAL;
}
x = next();
}
}
Token scanComplexName(int x) {
// name with unicode escape sequences
List<int> buffer = List<int>.from(input.getRange(tokenStart, index));
while (true) {
if (x == char.BACKSLASH) {
x = next();
if (x != char.$u) {
fail("Invalid escape sequence in name");
}
++index;
buffer.add(scanHexSequence(4));
x = current;
} else if (isNamePart(x)) {
buffer.add(x);
x = next();
} else {
break;
}
}
Token tok = emitValueToken(Token.NAME);
tok.value = String.fromCharCodes(buffer);
return tok..binaryPrecedence = Precedence.RELATIONAL;
}
/// [index] must point to the first hex digit.
/// It will be advanced to point AFTER the hex sequence (i.e. index += count).
int scanHexSequence(int count) {
int x = current;
int value = 0;
for (int i = 0; i < count; i++) {
if (char.$0 <= x && x <= char.$9) {
value = (value << 4) + (x - char.$0);
} else if (char.$a <= x && x <= char.$f) {
value = (value << 4) + (x - char.$a + 10);
} else if (char.$A <= x && x <= char.$F) {
value = (value << 4) + (x - char.$A + 10);
} else {
fail('Invalid hex sequence');
}
x = next();
}
return value;
}
Token scan() {
seenLinebreak = false;
scanLoop:
while (true) {
int x = current;
tokenStart = index;
tokenLine = currentLine;
switch (x) {
case char.NULL:
return emitToken(Token
.EOF); // (will produce infinite EOF tokens if pressed for more tokens)
case char
.SPACE: // Note: Exotic whitespace symbols are handled in the default clause.
case char.TAB:
++index;
continue;
case char.CR:
seenLinebreak = true;
++currentLine;
x = next();
if (x == char.LF) {
++index; // count as single linebreak
}
continue;
case char.LF:
case char.LS:
case char.PS:
seenLinebreak = true;
++currentLine;
++index;
continue;
case char.SLASH:
x = next(); // consume "/"
if (x == char.SLASH) {
// "//" comment
x = next();
while (!isEOL(x)) {
x = next();
}
continue; // line number will be when reading the LF,CR,LS,PS or EOF
}
if (x == char.STAR) {
// "/*" comment
x = current;
while (true) {
switch (x) {
case char.STAR:
x = next();
if (x == char.SLASH) {
++index; // skip final slash
continue scanLoop; // Finished block comment.
}
break;
case char.NULL:
fail("Unterminated block comment");
break;
case char.CR:
++currentLine;
x = next();
if (x == char.LF) {
x = next(); // count as one line break
}
break;
case char.LS:
case char.LF:
case char.PS:
++currentLine;
x = next();
break;
default:
x = next();
}
}
}
// parser will recognize these as potential regexp heads
if (x == char.EQ) {
// "/="
++index;
return emitToken(Token.ASSIGN, '/=');
}
return emitToken(Token.BINARY, '/')
..binaryPrecedence = Precedence.MULTIPLICATIVE;
case char.PLUS:
x = next();
if (x == char.PLUS) {
++index;
return emitToken(Token.UPDATE, '++');
}
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '+=');
}
return emitToken(Token.UNARY, '+')
..binaryPrecedence = Precedence.ADDITIVE;
case char.MINUS:
x = next();
if (x == char.MINUS) {
++index;
return emitToken(Token.UPDATE, '--');
}
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '-=');
}
return emitToken(Token.UNARY, '-')
..binaryPrecedence = Precedence.ADDITIVE;
case char.STAR:
x = next();
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '*=');
}
return emitToken(Token.BINARY, '*')
..binaryPrecedence = Precedence.MULTIPLICATIVE;
case char.PERCENT:
x = next();
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '%=');
}
return emitToken(Token.BINARY, '%')
..binaryPrecedence = Precedence.MULTIPLICATIVE;
case char.LT:
x = next();
if (x == char.LT) {
x = next();
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '<<=');
}
return emitToken(Token.BINARY, '<<')
..binaryPrecedence = Precedence.SHIFT;
}
if (x == char.EQ) {
++index;
return emitToken(Token.BINARY, '<=')
..binaryPrecedence = Precedence.RELATIONAL;
}
return emitToken(Token.BINARY, '<')
..binaryPrecedence = Precedence.RELATIONAL;
case char.GT:
x = next();
if (x == char.GT) {
x = next();
if (x == char.GT) {
x = next();
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '>>>=');
}
return emitToken(Token.BINARY, '>>>')
..binaryPrecedence = Precedence.SHIFT;
}
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '>>=');
}
return emitToken(Token.BINARY, '>>')
..binaryPrecedence = Precedence.SHIFT;
}
if (x == char.EQ) {
++index;
return emitToken(Token.BINARY, '>=')
..binaryPrecedence = Precedence.RELATIONAL;
}
return emitToken(Token.BINARY, '>')
..binaryPrecedence = Precedence.RELATIONAL;
case char.HAT:
x = next();
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '^=');
}
return emitToken(Token.BINARY, '^')
..binaryPrecedence = Precedence.BITWISE_XOR;
case char.TILDE:
++index;
return emitToken(Token.UNARY, '~');
case char.BAR:
x = next();
if (x == char.BAR) {
++index;
return emitToken(Token.BINARY, '||')
..binaryPrecedence = Precedence.LOGICAL_OR;
}
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '|=');
}
return emitToken(Token.BINARY, '|')
..binaryPrecedence = Precedence.BITWISE_OR;
case char.AMPERSAND:
x = next();
if (x == char.AMPERSAND) {
++index;
return emitToken(Token.BINARY, '&&')
..binaryPrecedence = Precedence.LOGICAL_AND;
}
if (x == char.EQ) {
++index;
return emitToken(Token.ASSIGN, '&=');
}
return emitToken(Token.BINARY, '&')
..binaryPrecedence = Precedence.BITWISE_AND;
case char.EQ:
x = next();
if (x == char.EQ) {
x = next();
if (x == char.EQ) {
++index;
return emitToken(Token.BINARY, '===')
..binaryPrecedence = Precedence.EQUALITY;
}
return emitToken(Token.BINARY, '==')
..binaryPrecedence = Precedence.EQUALITY;
}
return emitToken(Token.ASSIGN, '=');
case char.BANG:
x = next();
if (x == char.EQ) {
x = next();
if (x == char.EQ) {
++index;
return emitToken(Token.BINARY, '!==')
..binaryPrecedence = Precedence.EQUALITY;
}
return emitToken(Token.BINARY, '!=')
..binaryPrecedence = Precedence.EQUALITY;
}
return emitToken(Token.UNARY, '!');
case char.DOT:
x = next();
if (isDigit(x)) {
return scanDecimalPart(x);
}
return emitToken(Token.DOT);
case char.SQUOTE:
case char.DQUOTE:
return scanStringLiteral(x);
case char.LPAREN:
case char.RPAREN:
case char.LBRACKET:
case char.RBRACKET:
case char.LBRACE:
case char.RBRACE:
case char.COMMA:
case char.COLON:
case char.SEMICOLON:
case char.QUESTION:
++index;
return emitToken(x);
case char.BACKSLASH:
return scanComplexName(x);
default:
if (isNameStart(x)) return scanName(x);
if (isDigit(x)) return scanNumber(x);
if (isWhitespace(x)) {
++index;
continue;
}
fail(
"Unrecognized character: '${String.fromCharCode(x)}' (UTF+${x.toRadixString(16)})");
}
}
}
/// Scan a regular expression literal, where the opening token has already been scanned
/// This is called directly from the parser.
/// The opening token [slash] can be a "/" or a "/=" token
Token scanRegexpBody(Token slash) {
bool inCharClass =
false; // If true, we are inside a bracket. A slash in here does not terminate the literal. They are not nestable.
int x = current;
while (inCharClass || x != char.SLASH) {
switch (x) {
case char.NULL:
fail("Unterminated regexp");
break;
case char.LBRACKET:
inCharClass = true;
break;
case char.RBRACKET:
inCharClass = false;
break;
case char.BACKSLASH:
x = next();
if (isEOL(x)) fail("Unterminated regexp");
break;
case char.CR:
case char.LF:
case char.LS:
case char.PS:
fail("Unterminated regexp");
}
x = next();
}
x = next(); // Move past the terminating "/"
while (isNamePart(x)) {
// Parse flags
x = next();
}
return emitToken(Token.REGEXP,
String.fromCharCodes(input.getRange(slash.startOffset, index)));
}
Token scanStringLiteral(int x) {
List<int> buffer =
<int>[]; // String value without quotes, after resolving escapes.
int quote = x;
x = next();
while (x != quote) {
if (x == char.BACKSLASH) {
x = next();
switch (x) {
case char.$b:
buffer.add(char.BS);
x = next();
break;
case char.$f:
buffer.add(char.FF);
x = next();
break;
case char.$n:
buffer.add(char.LF);
x = next();
break;
case char.$r:
buffer.add(char.CR);
x = next();
break;
case char.$t:
buffer.add(char.TAB);
x = next();
break;
case char.$v:
buffer.add(char.VTAB);
x = next();
break;
case char.$x:
++index;
buffer.add(scanHexSequence(2));
x = current;
break;
case char.$u:
++index;
buffer.add(scanHexSequence(4));
x = current;
break;
case char.$0:
case char.$1:
case char.$2:
case char.$3:
case char.$4:
case char.$5:
case char.$6:
case char.$7: // Octal escape
int value = x - char.$0;
x = next();
while (isDigit(x)) {
int nextValue = (value << 3) + (x - char.$0);
if (nextValue > 127) break;
value = nextValue;
x = next();
}
buffer.add(value);
break; // OK
case char.LF:
case char.LS:
case char.PS:
++currentLine;
x = next(); // just continue on next line
break;
case char.CR:
++currentLine;
x = next();
if (x == char.LF) {
x = next(); // Escape entire CR-LF sequence
}
break;
case char.SQUOTE:
case char.DQUOTE:
case char.BACKSLASH:
default:
buffer.add(x);
x = next();
break;
}
} else if (isEOL(x)) {
// Note: EOF counts as an EOL
fail("Unterminated string literal");
} else {
buffer.add(x); // ordinary char
x = next();
}
}
++index; // skip ending quote
String value = String.fromCharCodes(buffer);
return emitValueToken(Token.STRING)..value = value;
}
}

View File

@ -0,0 +1,110 @@
library noise;
import 'charcode.dart' as char;
import 'lexer.dart' show isEOL, isWhitespace;
// ignore_for_file: prefer_single_quotes
// ignore_for_file: annotate_overrides
// ignore_for_file: always_declare_return_types
// ignore_for_file: avoid_renaming_method_parameters
// ignore_for_file: constant_identifier_names
// ignore_for_file: omit_local_variable_types
class Offsets {
int start;
int end;
int line;
Offsets(this.start, this.end, this.line);
}
/// Detects noise surrounding the source code and adjusts initial and ending offsets to ignore the noise.
///
/// The following things are considered noise:
/// - Hash bang: e.g. "#!/usr/bin/env node"
/// - HTML comment: <!--, -->
/// - HTML cdata tag: <![CDATA[, ]]>
Offsets trimNoise(String text, Offsets offsets) {
int index = offsets.start;
int end = offsets.end;
int currentLine = offsets.line;
bool lookahead(String str) {
if (index + str.length > end) return false;
for (int i = 0; i < str.length; i++) {
if (text.codeUnitAt(index + i) != str.codeUnitAt(i)) return false;
}
return true;
}
bool lookback(String str) {
if (str.length > end) return false;
for (int i = 0; i < str.length; i++) {
if (text.codeUnitAt(end - str.length + i) != str.codeUnitAt(i)) {
return false;
}
}
return true;
}
int next() {
++index;
return index == end ? char.NULL : text.codeUnitAt(index);
}
// Skip line with #!
if (lookahead('#!')) {
index += 2;
while (index < end && !isEOL(text.codeUnitAt(index))) {
++index;
}
}
// Skip whitespace until potential HTML comment marker
loop:
while (true) {
int x = text.codeUnitAt(index);
switch (x) {
case char.LF:
case char.LS:
case char.PS:
currentLine += 1;
x = next();
break;
case char.CR:
currentLine += 1;
x = next();
if (x == char.LF) {
x = next();
}
break;
default:
if (isWhitespace(x)) {
x = next();
} else {
break loop;
}
}
}
// Skip <!-- and <![CDATA[
if (lookahead('<!--')) {
index += '<!--'.length;
} else if (lookahead('<![CDATA[')) {
index += '<![CDATA['.length;
}
// Skip suffix whitespace (this is simpler than above since we do not need to update the line counter)
while (end > 0) {
int x = text.codeUnitAt(end - 1);
if (!isWhitespace(x) && !isEOL(x)) break;
--end;
}
// Check for trailing --> or ]]>
if (lookback('-->')) {
end -= '-->'.length;
} else if (lookback(']]>')) {
end -= ']]>'.length;
}
return Offsets(index, end, currentLine);
}

View File

@ -0,0 +1,957 @@
library parser;
import 'lexer.dart';
import 'ast.dart';
// ignore_for_file: prefer_single_quotes
// ignore_for_file: annotate_overrides
// ignore_for_file: always_declare_return_types
// ignore_for_file: avoid_renaming_method_parameters
// ignore_for_file: constant_identifier_names
// ignore_for_file: omit_local_variable_types
class Parser {
Parser(this.lexer) {
token = lexer.scan();
}
String get filename => lexer.filename;
Lexer lexer;
Token token;
/// End offset of the last consumed token (i.e. not the one in [token] but the one before that)
int endOffset;
dynamic fail({Token tok, String expected, String message}) {
tok ??= token;
if (message == null) {
if (expected != null) {
message = "Expected $expected but found $tok";
} else {
message = "Unexpected token $tok";
}
}
throw ParseError(
message, filename, tok.line, tok.startOffset, tok.endOffset);
}
/// Returns the current token, and scans the next one.
Token next() {
Token t = token;
endOffset = t.endOffset;
token = lexer.scan();
return t;
}
/// Consume a semicolon, or if a line-break was here, just pretend there was one here.
void consumeSemicolon() {
if (token.type == Token.SEMICOLON) {
next();
return;
}
if (token.afterLinebreak ||
token.type == Token.RBRACE ||
token.type == Token.EOF) {
return;
}
fail(expected: 'semicolon');
}
void consume(int type) {
if (token.type != type) {
fail(expected: Token.typeToString(type));
}
next();
}
Token requireNext(int type) {
if (token.type != type) {
fail(expected: Token.typeToString(type));
}
return next();
}
void consumeName(String name) {
if (token.type != Token.NAME || token.text != name) {
fail(expected: name);
}
next();
}
bool peekName(String name) {
return token.type == Token.NAME && token.text == name;
}
bool tryName(String name) {
if (token.type == Token.NAME && token.text == name) {
next();
return true;
} else {
return false;
}
}
Name makeName(Token tok) => Name(tok.value)
..start = tok.startOffset
..end = tok.endOffset
..line = tok.line;
Name parseName() => makeName(requireNext(Token.NAME));
///// FUNCTIONS //////
List<Name> parseParameters() {
consume(Token.LPAREN);
List<Name> list = <Name>[];
while (token.type != Token.RPAREN) {
if (list.isNotEmpty) {
consume(Token.COMMA);
}
list.add(parseName());
}
consume(Token.RPAREN);
return list;
}
BlockStatement parseFunctionBody() {
return parseBlock();
}
FunctionNode parseFunction() {
int start = token.startOffset;
assert(token.text == 'function');
Token funToken = next();
Name name;
if (token.type == Token.NAME) {
name = parseName();
}
List<Name> params = parseParameters();
BlockStatement body = parseFunctionBody();
return FunctionNode(name, params, body)
..start = start
..end = endOffset
..line = funToken.line;
}
///// EXPRESSIONS //////
Expression parsePrimary() {
int start = token.startOffset;
switch (token.type) {
case Token.NAME:
switch (token.text) {
case 'this':
Token tok = next();
return ThisExpression()
..start = start
..end = endOffset
..line = tok.line;
case 'true':
Token tok = next();
return LiteralExpression(true, 'true')
..start = start
..end = endOffset
..line = tok.line;
case 'false':
Token tok = next();
return LiteralExpression(false, 'false')
..start = start
..end = endOffset
..line = tok.line;
case 'null':
Token tok = next();
return LiteralExpression(null, 'null')
..start = start
..end = endOffset
..line = tok.line;
case 'function':
return FunctionExpression(parseFunction());
}
Name name = parseName();
return NameExpression(name)
..start = start
..end = endOffset
..line = name.line;
case Token.NUMBER:
Token tok = next();
return LiteralExpression(num.parse(tok.text), tok.text)
..start = start
..end = endOffset
..line = tok.line;
case Token.STRING:
Token tok = next();
return LiteralExpression(tok.value, tok.text)
..start = start
..end = endOffset
..line = tok.line;
case Token.LBRACKET:
return parseArrayLiteral();
case Token.LBRACE:
return parseObjectLiteral();
case Token.LPAREN:
next();
Expression exp = parseExpression();
consume(Token.RPAREN);
return exp;
case Token.BINARY:
case Token.ASSIGN:
if (token.text == '/' || token.text == '/=') {
Token regexTok = lexer.scanRegexpBody(token);
token = lexer.scan();
endOffset = regexTok.endOffset;
return RegexpExpression(regexTok.text)
..start = regexTok.startOffset
..end = regexTok.endOffset
..line = regexTok.line;
}
throw fail();
default:
throw fail();
}
}
Expression parseArrayLiteral() {
int start = token.startOffset;
Token open = requireNext(Token.LBRACKET);
List<Expression> expressions = <Expression>[];
while (token.type != Token.RBRACKET) {
if (token.type == Token.COMMA) {
next();
expressions.add(null);
} else {
expressions.add(parseAssignment());
if (token.type != Token.RBRACKET) {
consume(Token.COMMA);
}
}
}
consume(Token.RBRACKET);
return ArrayExpression(expressions)
..start = start
..end = endOffset
..line = open.line;
}
Node makePropertyName(Token tok) {
int start = tok.startOffset;
int end = tok.endOffset;
int line = tok.line;
switch (tok.type) {
case Token.NAME:
return Name(tok.text)
..start = start
..end = end
..line = line;
case Token.STRING:
return LiteralExpression(tok.value)
..raw = tok.text
..start = start
..end = end
..line = line;
case Token.NUMBER:
return LiteralExpression(double.parse(tok.text))
..raw = tok.text
..start = start
..end = end
..line = line;
default:
throw fail(tok: tok, expected: 'property name');
}
}
Property parseProperty() {
int start = token.startOffset;
Token nameTok = next();
if (token.type == Token.COLON) {
int line = token.line;
next(); // skip colon
Node name = makePropertyName(nameTok);
Expression value = parseAssignment();
return Property(name, value)
..start = start
..end = endOffset
..line = line;
}
if (nameTok.type == Token.NAME &&
(nameTok.text == 'get' || nameTok.text == 'set')) {
Token kindTok = nameTok;
String kind =
kindTok.text == 'get' ? 'get' : 'set'; // internalize the string
nameTok = next();
Node name = makePropertyName(nameTok);
int lparen = token.startOffset;
List<Name> params = parseParameters();
BlockStatement body = parseFunctionBody();
Node value = FunctionNode(null, params, body)
..start = lparen
..end = endOffset
..line = name.line;
return Property(name, value, kind)
..start = start
..end = endOffset
..line = kindTok.line;
}
throw fail(expected: 'property', tok: nameTok);
}
Expression parseObjectLiteral() {
int start = token.startOffset;
Token open = requireNext(Token.LBRACE);
List<Property> properties = <Property>[];
while (token.type != Token.RBRACE) {
if (properties.isNotEmpty) {
consume(Token.COMMA);
}
if (token.type == Token.RBRACE) break; // may end with extra comma
properties.add(parseProperty());
}
requireNext(Token.RBRACE);
return ObjectExpression(properties)
..start = start
..end = endOffset
..line = open.line;
}
List<Expression> parseArguments() {
consume(Token.LPAREN);
List<Expression> list = <Expression>[];
while (token.type != Token.RPAREN) {
if (list.isNotEmpty) {
consume(Token.COMMA);
}
list.add(parseAssignment());
}
consume(Token.RPAREN);
return list;
}
Expression parseMemberExpression(Token newTok) {
int start = token.startOffset;
Expression exp = parsePrimary();
loop:
while (true) {
int line = token.line;
switch (token.type) {
case Token.DOT:
next();
Name name = parseName();
exp = MemberExpression(exp, name)
..start = start
..end = endOffset
..line = line;
break;
case Token.LBRACKET:
next();
Expression index = parseExpression();
requireNext(Token.RBRACKET);
exp = IndexExpression(exp, index)
..start = start
..end = endOffset
..line = line;
break;
case Token.LPAREN:
List<Expression> args = parseArguments();
if (newTok != null) {
start = newTok.startOffset;
exp = CallExpression.newCall(exp, args)
..start = start
..end = endOffset
..line = line;
newTok = null;
} else {
exp = CallExpression(exp, args)
..start = start
..end = endOffset
..line = line;
}
break;
default:
break loop;
}
}
if (newTok != null) {
exp = CallExpression.newCall(exp, <Expression>[])
..start = newTok.startOffset
..end = endOffset
..line = newTok.line;
}
return exp;
}
Expression parseNewExpression() {
assert(token.text == 'new');
Token newTok = next();
if (peekName('new')) {
Expression exp = parseNewExpression();
return CallExpression.newCall(exp, <Expression>[])
..start = newTok.startOffset
..end = endOffset
..line = newTok.line;
}
return parseMemberExpression(newTok);
}
Expression parseLeftHandSide() {
if (peekName('new')) {
return parseNewExpression();
} else {
return parseMemberExpression(null);
}
}
Expression parsePostfix() {
int start = token.startOffset;
Expression exp = parseLeftHandSide();
if (token.type == Token.UPDATE && !token.afterLinebreak) {
Token operator = next();
exp = UpdateExpression.postfix(operator.text, exp)
..start = start
..end = endOffset
..line = operator.line;
}
return exp;
}
Expression parseUnary() {
switch (token.type) {
case Token.UNARY:
Token operator = next();
Expression exp = parseUnary();
return UnaryExpression(operator.text, exp)
..start = operator.startOffset
..end = endOffset
..line = operator.line;
case Token.UPDATE:
Token operator = next();
Expression exp = parseUnary();
return UpdateExpression.prefix(operator.text, exp)
..start = operator.startOffset
..end = endOffset
..line = operator.line;
case Token.NAME:
if (token.text == 'delete' ||
token.text == 'void' ||
token.text == 'typeof') {
Token operator = next();
Expression exp = parseUnary();
return UnaryExpression(operator.text, exp)
..start = operator.startOffset
..end = endOffset
..line = operator.line;
}
break;
}
return parsePostfix();
}
Expression parseBinary(int minPrecedence, bool allowIn) {
int start = token.startOffset;
Expression exp = parseUnary();
while (token.binaryPrecedence >= minPrecedence) {
if (token.type == Token.NAME) {
// All name tokens are given precedence of RELATIONAL
// Weed out name tokens that are not actually binary operators
if (token.value != 'instanceof' && (token.value != 'in' || !allowIn)) {
break;
}
}
Token operator = next();
Expression right = parseBinary(operator.binaryPrecedence + 1, allowIn);
exp = BinaryExpression(exp, operator.text, right)
..start = start
..end = endOffset
..line = operator.line;
}
return exp;
}
Expression parseConditional(bool allowIn) {
int start = token.startOffset;
Expression exp = parseBinary(Precedence.EXPRESSION, allowIn);
if (token.type == Token.QUESTION) {
Token quest = next();
Expression thenExp = parseAssignment();
consume(Token.COLON);
Expression elseExp = parseAssignment(allowIn: allowIn);
exp = ConditionalExpression(exp, thenExp, elseExp)
..start = start
..end = endOffset
..line = quest.line;
}
return exp;
}
Expression parseAssignment({bool allowIn = true}) {
int start = token.startOffset;
Expression exp = parseConditional(allowIn);
if (token.type == Token.ASSIGN) {
Token operator = next();
Expression right = parseAssignment(allowIn: allowIn);
exp = AssignmentExpression(exp, operator.text, right)
..start = start
..end = endOffset
..line = operator.line;
}
return exp;
}
Expression parseExpression({bool allowIn = true}) {
int start = token.startOffset;
Expression exp = parseAssignment(allowIn: allowIn);
if (token.type == Token.COMMA) {
List<Expression> expressions = <Expression>[exp];
while (token.type == Token.COMMA) {
next();
expressions.add(parseAssignment(allowIn: allowIn));
}
exp = SequenceExpression(expressions)
..start = start
..end = endOffset
..line = expressions.first.line;
}
return exp;
}
////// STATEMENTS /////
BlockStatement parseBlock() {
int start = token.startOffset;
int line = token.line;
consume(Token.LBRACE);
List<Statement> list = <Statement>[];
while (token.type != Token.RBRACE) {
list.add(parseStatement());
}
consume(Token.RBRACE);
return BlockStatement(list)
..start = start
..end = endOffset
..line = line;
}
VariableDeclaration parseVariableDeclarationList({bool allowIn = true}) {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'var');
consume(Token.NAME);
List<VariableDeclarator> list = <VariableDeclarator>[];
while (true) {
Name name = parseName();
Expression init;
if (token.type == Token.ASSIGN) {
if (token.text != '=') {
fail(message: 'Compound assignment in initializer');
}
next();
init = parseAssignment(allowIn: allowIn);
}
list.add(VariableDeclarator(name, init)
..start = name.start
..end = endOffset
..line = name.line);
if (token.type != Token.COMMA) break;
next();
}
return VariableDeclaration(list)
..start = start
..end = endOffset
..line = line;
}
VariableDeclaration parseVariableDeclarationStatement() {
VariableDeclaration decl = parseVariableDeclarationList();
consumeSemicolon();
return decl..end = endOffset; // overwrite end so semicolon is included
}
Statement parseEmptyStatement() {
Token semi = requireNext(Token.SEMICOLON);
return EmptyStatement()
..start = semi.startOffset
..end = semi.endOffset
..line = semi.line;
}
Statement parseExpressionStatement() {
int start = token
.startOffset; // Note: not the same as exp.start due to removal of parentheses
Expression exp = parseExpression();
consumeSemicolon();
return ExpressionStatement(exp)
..start = start
..end = endOffset
..line = exp.line;
}
Statement parseExpressionOrLabeledStatement() {
int start = token
.startOffset; // Note: not the same as exp.start due to removal of parentheses
Expression exp = parseExpression();
if (token.type == Token.COLON &&
exp is NameExpression &&
exp.start == start) {
Name name = exp.name;
next(); // skip the colon
Statement inner = parseStatement();
return LabeledStatement(name, inner);
} else {
consumeSemicolon();
return ExpressionStatement(exp)
..start = start
..end = endOffset
..line = exp.line;
}
}
Statement parseIf() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'if');
consume(Token.NAME);
consume(Token.LPAREN);
Expression condition = parseExpression();
consume(Token.RPAREN);
Statement thenBody = parseStatement();
Statement elseBody;
if (tryName('else')) {
elseBody = parseStatement();
}
return IfStatement(condition, thenBody, elseBody)
..start = start
..end = endOffset
..line = line;
}
Statement parseDoWhile() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'do');
consume(Token.NAME);
Statement body = parseStatement();
consumeName('while');
consume(Token.LPAREN);
Expression condition = parseExpression();
consume(Token.RPAREN);
consumeSemicolon();
return DoWhileStatement(body, condition)
..start = start
..end = endOffset
..line = line;
}
Statement parseWhile() {
int line = token.line;
int start = token.startOffset;
assert(token.text == 'while');
consume(Token.NAME);
consume(Token.LPAREN);
Expression condition = parseExpression();
consume(Token.RPAREN);
Statement body = parseStatement();
return WhileStatement(condition, body)
..start = start
..end = endOffset
..line = line;
}
Statement parseFor() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'for');
consume(Token.NAME);
consume(Token.LPAREN);
Node exp1;
if (peekName('var')) {
exp1 = parseVariableDeclarationList(allowIn: false);
} else if (token.type != Token.SEMICOLON) {
exp1 = parseExpression(allowIn: false);
}
if (exp1 != null && tryName('in')) {
if (exp1 is VariableDeclaration && exp1.declarations.length > 1) {
fail(message: 'Multiple vars declared in for-in loop');
}
Expression exp2 = parseExpression();
consume(Token.RPAREN);
Statement body = parseStatement();
return ForInStatement(exp1, exp2, body)
..start = start
..end = endOffset
..line = line;
} else {
consume(Token.SEMICOLON);
Expression exp2, exp3;
if (token.type != Token.SEMICOLON) {
exp2 = parseExpression();
}
consume(Token.SEMICOLON);
if (token.type != Token.RPAREN) {
exp3 = parseExpression();
}
consume(Token.RPAREN);
Statement body = parseStatement();
return ForStatement(exp1, exp2, exp3, body)
..start = start
..end = endOffset
..line = line;
}
}
Statement parseContinue() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'continue');
consume(Token.NAME);
Name name;
if (token.type == Token.NAME && !token.afterLinebreak) {
name = parseName();
}
consumeSemicolon();
return ContinueStatement(name)
..start = start
..end = endOffset
..line = line;
}
Statement parseBreak() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'break');
consume(Token.NAME);
Name name;
if (token.type == Token.NAME && !token.afterLinebreak) {
name = parseName();
}
consumeSemicolon();
return BreakStatement(name)
..start = start
..end = endOffset
..line = line;
}
Statement parseReturn() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'return');
consume(Token.NAME);
Expression exp;
if (token.type != Token.SEMICOLON &&
token.type != Token.RBRACE &&
token.type != Token.EOF &&
!token.afterLinebreak) {
exp = parseExpression();
}
consumeSemicolon();
return ReturnStatement(exp)
..start = start
..end = endOffset
..line = line;
}
Statement parseWith() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'with');
consume(Token.NAME);
consume(Token.LPAREN);
Expression exp = parseExpression();
consume(Token.RPAREN);
Statement body = parseStatement();
return WithStatement(exp, body)
..start = start
..end = endOffset
..line = line;
}
Statement parseSwitch() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'switch');
consume(Token.NAME);
consume(Token.LPAREN);
Expression argument = parseExpression();
consume(Token.RPAREN);
consume(Token.LBRACE);
List<SwitchCase> cases = <SwitchCase>[];
cases.add(parseSwitchCaseHead());
while (token.type != Token.RBRACE) {
if (peekName('case') || peekName('default')) {
cases.add(parseSwitchCaseHead());
} else {
cases.last.body.add(parseStatement());
cases.last.end = endOffset;
}
}
consume(Token.RBRACE);
return SwitchStatement(argument, cases)
..start = start
..end = endOffset
..line = line;
}
/// Parses a single 'case E:' or 'default:' without the following statements
SwitchCase parseSwitchCaseHead() {
int start = token.startOffset;
int line = token.line;
Token tok = requireNext(Token.NAME);
if (tok.text == 'case') {
Expression value = parseExpression();
consume(Token.COLON);
return SwitchCase(value, <Statement>[])
..start = start
..end = endOffset
..line = line;
} else if (tok.text == 'default') {
consume(Token.COLON);
return SwitchCase(null, <Statement>[])
..start = start
..end = endOffset
..line = line;
} else {
throw fail();
}
}
Statement parseThrow() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'throw');
consume(Token.NAME);
Expression exp = parseExpression();
consumeSemicolon();
return ThrowStatement(exp)
..start = start
..end = endOffset
..line = line;
}
Statement parseTry() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'try');
consume(Token.NAME);
BlockStatement body = parseBlock();
CatchClause handler;
BlockStatement finalizer;
if (peekName('catch')) {
Token catchTok = next();
consume(Token.LPAREN);
Name name = parseName();
consume(Token.RPAREN);
BlockStatement catchBody = parseBlock();
handler = CatchClause(name, catchBody)
..start = catchTok.startOffset
..end = endOffset
..line = catchTok.line;
}
if (tryName('finally')) {
finalizer = parseBlock();
}
return TryStatement(body, handler, finalizer)
..start = start
..end = endOffset
..line = line;
}
Statement parseDebuggerStatement() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'debugger');
consume(Token.NAME);
consumeSemicolon();
return DebuggerStatement()
..start = start
..end = endOffset
..line = line;
}
Statement parseFunctionDeclaration() {
int start = token.startOffset;
int line = token.line;
assert(token.text == 'function');
FunctionNode func = parseFunction();
if (func.name == null) {
fail(message: 'Function declaration must have a name');
}
return FunctionDeclaration(func)
..start = start
..end = endOffset
..line = line;
}
Statement parseStatement() {
if (token.type == Token.LBRACE) return parseBlock();
if (token.type == Token.SEMICOLON) return parseEmptyStatement();
if (token.type != Token.NAME) return parseExpressionStatement();
switch (token.value) {
case 'var':
return parseVariableDeclarationStatement();
case 'if':
return parseIf();
case 'do':
return parseDoWhile();
case 'while':
return parseWhile();
case 'for':
return parseFor();
case 'continue':
return parseContinue();
case 'break':
return parseBreak();
case 'return':
return parseReturn();
case 'with':
return parseWith();
case 'switch':
return parseSwitch();
case 'throw':
return parseThrow();
case 'try':
return parseTry();
case 'debugger':
return parseDebuggerStatement();
case 'function':
return parseFunctionDeclaration();
default:
return parseExpressionOrLabeledStatement();
}
}
Program parseProgram() {
int start = token.startOffset;
int line = token.line;
List<Statement> statements = <Statement>[];
while (token.type != Token.EOF) {
statements.add(parseStatement());
}
endOffset ??= start;
return Program(statements)
..start = start
..end = endOffset
..line = line;
}
Program parseExpressionProgram() {
int start = token.startOffset;
int line = token.line;
var statement = parseExpressionStatement();
consume(Token.EOF);
endOffset ??= start;
return Program(<Statement>[statement])
..start = start
..end = endOffset
..line = line;
}
}

View File

@ -0,0 +1,311 @@
library symbol_table;
part 'variable.dart';
part 'visibility.dart';
/// A hierarchical mechanism to hold a set of variables, which supports scoping and constant variables.
class SymbolTable<T> {
final List<SymbolTable<T>> _children = [];
final Map<String, Variable<T>> _lookupCache = {};
final Map<String, int> _names = {};
final List<Variable<T>> _variables = [];
int _depth = 0;
T _context;
SymbolTable<T> _parent, _root;
/// Initializes an empty symbol table.
///
/// You can optionally provide a [Map] of starter [values].
SymbolTable({Map<String, T> values = const {}}) {
if (values?.isNotEmpty == true) {
values.forEach((k, v) {
_variables.add(Variable<T>._(k, this, value: v));
});
}
}
/// Returns the nearest context this symbol table belongs to. Returns `null` if none was set within the entire tree.
///
/// This can be used to bind values to a `this` scope within a compiler.
T get context {
SymbolTable<T> search = this;
while (search != null) {
if (search._context != null) return search._context;
search = search._parent;
}
return null;
}
/// Sets a local context for values within this scope to be resolved against.
void set context(T value) {
_context = value;
}
/// The depth of this symbol table within the tree. At the root, this is `0`.
int get depth => _depth;
/// Returns `true` if this scope has no parent.
bool get isRoot => _parent == null;
/// Gets the parent of this symbol table.
SymbolTable<T> get parent => _parent;
/// Resolves the symbol table at the very root of the hierarchy.
///
/// This value is memoized to speed up future lookups.
SymbolTable<T> get root {
if (_root != null) return _root;
SymbolTable<T> out = this;
while (out._parent != null) out = out._parent;
return _root = out;
}
/// Retrieves every variable within this scope and its ancestors.
///
/// Variable names will not be repeated; this produces the effect of
/// shadowed variables.
///
/// This list is unmodifiable.
List<Variable<T>> get allVariables {
List<String> distinct = [];
List<Variable<T>> out = [];
void crawl(SymbolTable<T> table) {
for (var v in table._variables) {
if (!distinct.contains(v.name)) {
distinct.add(v.name);
out.add(v);
}
}
if (table._parent != null) crawl(table._parent);
}
crawl(this);
return List<Variable<T>>.unmodifiable(out);
}
/// Helper for calling [allVariablesWithVisibility] to fetch all public variables.
List<Variable<T>> get allPublicVariables {
return allVariablesWithVisibility(Visibility.public);
}
/// Use [allVariablesWithVisibility] instead.
@deprecated
List<Variable<T>> allVariablesOfVisibility(Visibility visibility) {
return allVariablesWithVisibility(visibility);
}
/// Retrieves every variable of the given [visibility] within this scope and its ancestors.
///
/// Variable names will not be repeated; this produces the effect of
/// shadowed variables.
///
/// Use this to "export" symbols out of a library or class.
///
/// This list is unmodifiable.
List<Variable<T>> allVariablesWithVisibility(Visibility visibility) {
List<String> distinct = [];
List<Variable<T>> out = [];
void crawl(SymbolTable<T> table) {
for (var v in table._variables) {
if (!distinct.contains(v.name) && v.visibility == visibility) {
distinct.add(v.name);
out.add(v);
}
}
if (table._parent != null) crawl(table._parent);
}
crawl(this);
return List<Variable<T>>.unmodifiable(out);
}
Variable<T> operator [](String name) => resolve(name);
void operator []=(String name, T value) {
assign(name, value);
}
void _wipeLookupCache(String key) {
_lookupCache.remove(key);
_children.forEach((c) => c._wipeLookupCache(key));
}
/// Use [create] instead.
@deprecated
Variable<T> add(String name, {T value, bool constant}) {
return create(name, value: value, constant: constant);
}
/// Create a new variable *within this scope*.
///
/// You may optionally provide a [value], or mark the variable as [constant].
Variable<T> create(String name, {T value, bool constant}) {
// Check if it exists first.
if (_variables.any((v) => v.name == name))
throw StateError(
'A symbol named "$name" already exists within the current context.');
_wipeLookupCache(name);
Variable<T> v = Variable._(name, this, value: value);
if (constant == true) v.lock();
_variables.add(v);
return v;
}
/// Use [assign] instead.
@deprecated
Variable<T> put(String name, T value) {
return assign(name, value);
}
/// Assigns a [value] to the variable with the given [name], or creates a new variable.
///
/// You cannot use this method to assign constants.
///
/// Returns the variable whose value was just assigned.
Variable<T> assign(String name, T value) {
return resolveOrCreate(name)..value = value;
}
/// Removes the variable with the given [name] from this scope, or an ancestor.
///
/// Returns the deleted variable, or `null`.
///
/// *Note: This may cause [resolve] calls in [fork]ed scopes to return `null`.*
/// *Note: There is a difference between symbol tables created via [fork], [createdChild], and [clone].*
Variable<T> remove(String name) {
SymbolTable<T> search = this;
while (search != null) {
var variable = search._variables
.firstWhere((v) => v.name == name, orElse: () => null);
if (variable != null) {
search._wipeLookupCache(name);
search._variables.remove(variable);
return variable;
}
search = search._parent;
}
return null;
}
/// Finds the variable with the given name, either within this scope or an ancestor.
///
/// Returns `null` if none has been found.
Variable<T> resolve(String name) {
var v = _lookupCache.putIfAbsent(name, () {
var variable =
_variables.firstWhere((v) => v.name == name, orElse: () => null);
if (variable != null)
return variable;
else if (_parent != null)
return _parent.resolve(name);
else
return null;
});
if (v == null) {
_lookupCache.remove(name);
return null;
} else
return v;
}
/// Finds the variable with the given name, either within this scope or an ancestor.
/// Creates a new variable if none was found.
///
/// If a new variable is created, you may optionally give it a [value].
/// You can also mark the new variable as a [constant].
Variable<T> resolveOrCreate(String name, {T value, bool constant}) {
var resolved = resolve(name);
if (resolved != null) return resolved;
return create(name, value: value, constant: constant);
}
/// Creates a child scope within this one.
///
/// You may optionally provide starter [values].
SymbolTable<T> createChild({Map<String, T> values = const {}}) {
var child = SymbolTable(values: values);
child
.._depth = _depth + 1
.._parent = this
.._root = _root;
_children.add(child);
return child;
}
/// Creates a scope identical to this one, but with no children.
///
/// The [parent] scope will see the new scope as a child.
SymbolTable<T> clone() {
var table = SymbolTable<T>();
table._variables.addAll(_variables);
table
.._depth = _depth
.._parent = _parent
.._root = _root;
_parent?._children?.add(table);
return table;
}
/// Creates a *forked* scope, derived from this one.
/// You may provide starter [values].
///
/// As opposed to [createChild], all variables in the resulting forked
/// scope will be *copies* of those in this class. This makes forked
/// scopes useful for implementations of concepts like closure functions,
/// where the current values of variables are trapped.
///
/// The forked scope is essentially orphaned and stands alone; although its
/// [parent] getter will point to the parent of the original scope, the parent
/// will not be aware of the new scope's existence.
SymbolTable<T> fork({Map<String, T> values = const {}}) {
var table = SymbolTable<T>();
table
.._depth = _depth
.._parent = _parent
.._root = _root;
table._variables.addAll(_variables.map((Variable<T> v) {
Variable<T> variable = Variable._(v.name, this, value: v.value);
variable.visibility = v.visibility;
if (v.isImmutable) variable.lock();
return variable;
}));
return table;
}
/// Returns a variation on the input [name] that is guaranteed to never be repeated within this scope.
///
/// The variation will the input [name], but with a numerical suffix appended.
/// Ex. `foo1`, `bar24`
String uniqueName(String name) {
int count = 0;
SymbolTable search = this;
while (search != null) {
if (search._names.containsKey(name)) count += search._names[name];
search = search._parent;
}
_names.putIfAbsent(name, () => 0);
_names[name]++;
return '$name$count';
}
}

View File

@ -0,0 +1,51 @@
part of symbol_table;
/// Holds an immutable symbol, the value of which is set once and only once.
@deprecated
class Constant<T> extends Variable<T> {
Constant(String name, T value) : super._(name, null, value: value) {
lock();
}
}
/// Holds a symbol, the value of which may change or be marked immutable.
class Variable<T> {
final String name;
final SymbolTable<T> symbolTable;
Visibility visibility = Visibility.public;
bool _locked = false;
T _value;
Variable._(this.name, this.symbolTable, {T value}) {
_value = value;
}
/// If `true`, then the value of this variable cannot be overwritten.
bool get isImmutable => _locked;
/// This flag has no meaning within the context of this library, but if you
/// are implementing some sort of interpreter, you may consider acting based on
/// whether a variable is private.
@deprecated
bool get isPrivate => visibility == Visibility.private;
T get value => _value;
set value(T value) {
if (_locked) {
throw StateError('The value of constant "$name" cannot be overwritten.');
}
_value = value;
}
/// Locks this symbol, and prevents its [value] from being overwritten.
void lock() {
_locked = true;
}
/// Marks this symbol as private.
@deprecated
void markAsPrivate() {
visibility = Visibility.private;
}
}

View File

@ -0,0 +1,31 @@
part of symbol_table;
/// Represents the visibility of a symbol.
///
/// Symbols may be [public], [protected], or [private].
/// The significance of a symbol's visibility is semantic and specific to the interpreter/compiler;
/// this package attaches no specific meaning to it.
///
/// [Visibility] instances can be compared using the `<`, `<=`, `>`, and `>=` operators.
/// The evaluation of the aforementioned operators is logical;
/// for example, a [private] symbol is *less visible* than a [public] symbol,
/// so [private] < [public].
///
/// In a nutshell: [private] < [protected] < [public].
class Visibility implements Comparable<Visibility> {
static const Visibility private = Visibility._(0);
static const Visibility protected = Visibility._(1);
static const Visibility public = Visibility._(2);
final int _n;
const Visibility._(this._n);
bool operator >(Visibility other) => _n > other._n;
bool operator >=(Visibility other) => _n >= other._n;
bool operator <(Visibility other) => _n < other._n;
bool operator <=(Visibility other) => _n <= other._n;
@override
int compareTo(Visibility other) {
return _n.compareTo(other._n);
}
}

View File

@ -51,15 +51,16 @@ class AnimatablePathValueParser {
case 1:
if (reader.peek() == Token.string) {
hasExpressions = true;
reader.skipValue();
var expression = reader.nextString();
print('${composition.name} EXPRESSION 2: $expression');
} else {
xAnimation = AnimatableValueParser.parseFloat(reader, composition);
}
break;
case 2:
if (reader.peek() == Token.string) {
hasExpressions = true;
reader.skipValue();
var expression = reader.nextString();
print('${composition.name} EXPRESSION 3: $expression');
} else {
yAnimation = AnimatableValueParser.parseFloat(reader, composition);
}

View File

@ -15,6 +15,8 @@ class KeyframesParser {
var keyframes = <Keyframe<T>>[];
if (reader.peek() == Token.string) {
var expression = reader.nextString();
print('${composition.name} EXPRESSION 1: $expression');
composition.addWarning("Lottie doesn't support expressions.");
return keyframes;
}

64
test/expression_test.dart Normal file
View File

@ -0,0 +1,64 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:lottie/src/expression/parsejs/parsejs.dart';
import 'package:lottie/src/expression/interpreter/interpreter.dart';
void main() {
test('Expressions', () {
var expressions = <String>[
r'''
var $bm_rt;
var amp, freq, decay, n, n, t, t, v;
amp = 0.1;
freq = 1.5;
decay = 4;
$bm_rt = n = 0;
if (numKeys > 0) {
$bm_rt = n = nearestKey(time).index;
if (key(n).time > time) {
n--;
}
}
if (n == 0) {
$bm_rt = t = 0;
} else {
$bm_rt = t = sub(time, key(n).time);
}
if (n > 0) {
v = velocityAtTime(sub(key(n).time, div(thisComp.frameDuration, 10)));
$bm_rt = sum(value, div(mul(mul(v, amp), Math.sin(mul(mul(mul(freq, t), 2), Math.PI))), Math.exp(mul(decay, t))));
} else {
$bm_rt = value;
}
''',
];
for (var expression in expressions) {
var parsed = parsejs(expression);
var interpreter = Interpreter();
interpreter
..global.properties['nearestKey'] =
JsFunction(interpreter.global, (samurai, arguments, ctx) {
var object = JsObject();
object.properties['index'] = JsNumber(0);
return object;
})
..global.properties['key'] =
JsFunction(interpreter.global, (samurai, arguments, ctx) {
var object = JsObject();
object.properties['time'] = JsNumber(0);
return object;
})
..global.properties['sub'] =
JsFunction(interpreter.global, (samurai, arguments, ctx) {
var object = JsObject();
return object;
})
..global.properties['time'] = JsNumber(100)
..global.properties['value'] = JsNumber(100)
..globalScope.create('numKeys', value: JsNumber(3));
interpreter.visitProgram(parsed);
}
});
}

View File

@ -4,10 +4,13 @@ import 'package:lottie/src/parser/moshi/json_reader.dart';
void main() {
test('Read json', () {
var s = ';\nvar';
print(s.codeUnits);
var reader = JsonReader.fromBytes(utf8.encoder.convert(_simpleJson));
var messages = readMessagesArray(reader);
expect(messages, hasLength(2));
expect(messages.first.user.name, 'json_newb');
expect(messages.first.user.name, 'var bm_rt;\nvar ampb');
});
}
@ -96,7 +99,7 @@ final _simpleJson = '''
"text": "How do I read a JSON stream in Java?",
"geo": null,
"user": {
"name": "json_newb",
"name": "var bm_rt;\nvar ampb",
"followers_count": 41
}
},