mirror of
https://github.com/xvrh/lottie-flutter.git
synced 2025-08-06 16:39:36 +08:00
Compare commits
3 Commits
v2.7.0
...
expression
Author | SHA1 | Date | |
---|---|---|---|
4ed2d5fab5 | |||
c9cf30e79f | |||
7cc0130ad4 |
11
lib/src/expression/interpreter/interpreter.dart
Normal file
11
lib/src/expression/interpreter/interpreter.dart
Normal 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';
|
28
lib/src/expression/interpreter/src/arguments.dart
Normal file
28
lib/src/expression/interpreter/src/arguments.dart
Normal 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(',');
|
||||
}
|
||||
}
|
||||
}
|
64
lib/src/expression/interpreter/src/array.dart
Normal file
64
lib/src/expression/interpreter/src/array.dart
Normal 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() => '';
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
4
lib/src/expression/interpreter/src/builtin/builtin.dart
Normal file
4
lib/src/expression/interpreter/src/builtin/builtin.dart
Normal file
@ -0,0 +1,4 @@
|
||||
export 'boolean_constructor.dart';
|
||||
export 'functions.dart';
|
||||
export 'function_constructor.dart';
|
||||
export 'misc.dart';
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
13
lib/src/expression/interpreter/src/builtin/functions.dart
Normal file
13
lib/src/expression/interpreter/src/builtin/functions.dart
Normal 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),
|
||||
});
|
||||
}
|
109
lib/src/expression/interpreter/src/builtin/misc.dart
Normal file
109
lib/src/expression/interpreter/src/builtin/misc.dart
Normal 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'),
|
||||
});
|
||||
}
|
150
lib/src/expression/interpreter/src/console.dart
Normal file
150
lib/src/expression/interpreter/src/console.dart
Normal 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;
|
||||
}
|
||||
}
|
18
lib/src/expression/interpreter/src/context.dart
Normal file
18
lib/src/expression/interpreter/src/context.dart
Normal 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;
|
||||
}
|
||||
}
|
98
lib/src/expression/interpreter/src/function.dart
Normal file
98
lib/src/expression/interpreter/src/function.dart
Normal 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);
|
||||
}
|
478
lib/src/expression/interpreter/src/interpreter.dart
Normal file
478
lib/src/expression/interpreter/src/interpreter.dart
Normal 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;
|
||||
}
|
||||
}
|
71
lib/src/expression/interpreter/src/literal.dart
Normal file
71
lib/src/expression/interpreter/src/literal.dart
Normal 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';
|
||||
}
|
101
lib/src/expression/interpreter/src/object.dart
Normal file
101
lib/src/expression/interpreter/src/object.dart
Normal 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();
|
||||
}
|
59
lib/src/expression/interpreter/src/stack.dart
Normal file
59
lib/src/expression/interpreter/src/stack.dart
Normal 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();
|
||||
}
|
||||
}
|
90
lib/src/expression/interpreter/src/util.dart
Normal file
90
lib/src/expression/interpreter/src/util.dart
Normal 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;
|
||||
}
|
49
lib/src/expression/parsejs/parsejs.dart
Normal file
49
lib/src/expression/parsejs/parsejs.dart
Normal 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;
|
||||
}
|
111
lib/src/expression/parsejs/src/annotations.dart
Normal file
111
lib/src/expression/parsejs/src/annotations.dart
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
884
lib/src/expression/parsejs/src/ast.dart
Normal file
884
lib/src/expression/parsejs/src/ast.dart
Normal 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);
|
||||
}
|
260
lib/src/expression/parsejs/src/ast_visitor.dart
Normal file
260
lib/src/expression/parsejs/src/ast_visitor.dart
Normal 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);
|
||||
}
|
115
lib/src/expression/parsejs/src/charcode.dart
Normal file
115
lib/src/expression/parsejs/src/charcode.dart
Normal 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
|
728
lib/src/expression/parsejs/src/lexer.dart
Normal file
728
lib/src/expression/parsejs/src/lexer.dart
Normal 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;
|
||||
}
|
||||
}
|
110
lib/src/expression/parsejs/src/noise.dart
Normal file
110
lib/src/expression/parsejs/src/noise.dart
Normal 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);
|
||||
}
|
957
lib/src/expression/parsejs/src/parser.dart
Normal file
957
lib/src/expression/parsejs/src/parser.dart
Normal 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;
|
||||
}
|
||||
}
|
311
lib/src/expression/symbol_table/symbol_table.dart
Normal file
311
lib/src/expression/symbol_table/symbol_table.dart
Normal 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';
|
||||
}
|
||||
}
|
51
lib/src/expression/symbol_table/variable.dart
Normal file
51
lib/src/expression/symbol_table/variable.dart
Normal 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;
|
||||
}
|
||||
}
|
31
lib/src/expression/symbol_table/visibility.dart
Normal file
31
lib/src/expression/symbol_table/visibility.dart
Normal 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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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
64
test/expression_test.dart
Normal 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);
|
||||
}
|
||||
});
|
||||
}
|
@ -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
|
||||
}
|
||||
},
|
||||
|
Reference in New Issue
Block a user