322 lines
9.2 KiB
Dart
322 lines
9.2 KiB
Dart
library symbol_table;
|
|
|
|
import 'package:collection/collection.dart' show IterableExtension;
|
|
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.
|
|
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;
|
|
|
|
var 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 {
|
|
var distinct = <String>[];
|
|
var out = <Variable<T>>[];
|
|
|
|
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("allVariablesWithVisibility")
|
|
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) {
|
|
var distinct = <String>[];
|
|
var out = <Variable<T>>[];
|
|
|
|
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);
|
|
for (var c in _children) {
|
|
c._wipeLookupCache(key);
|
|
}
|
|
}
|
|
|
|
/// Use [create] instead.
|
|
@Deprecated("create")
|
|
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);
|
|
var v = Variable<T>._(name, this, value: value);
|
|
if (constant == true) v.lock();
|
|
_variables.add(v);
|
|
return v;
|
|
}
|
|
|
|
/// Use [assign] instead.
|
|
@Deprecated("assign")
|
|
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.firstWhereOrNull((v) => v.name == name);
|
|
|
|
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.firstWhereOrNull((v) => v.name == name);
|
|
|
|
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 v) {
|
|
var variable = Variable<T>._(v.name, this, value: v.value as T?);
|
|
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) {
|
|
var count = 0;
|
|
SymbolTable? search = this;
|
|
|
|
while (search != null) {
|
|
if (search._names.containsKey(name)) count += search._names[name]!;
|
|
search = search._parent;
|
|
}
|
|
|
|
_names.putIfAbsent(name, () => 0);
|
|
var n = _names[name];
|
|
if (n != null) {
|
|
n++;
|
|
_names[name] = n;
|
|
}
|
|
return '$name$count';
|
|
}
|
|
}
|