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 { final List> _children = []; final Map?> _lookupCache = {}; final Map _names = {}; final List> _variables = []; int _depth = 0; T? _context; SymbolTable? _parent, _root; /// Initializes an empty symbol table. /// /// You can optionally provide a [Map] of starter [values]. SymbolTable({Map values = const {}}) { if (values.isNotEmpty == true) { values.forEach((k, v) { _variables.add(Variable._(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? 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? get parent => _parent; /// Resolves the symbol table at the very root of the hierarchy. /// /// This value is memoized to speed up future lookups. SymbolTable? 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> get allVariables { var distinct = []; var out = >[]; void crawl(SymbolTable 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>.unmodifiable(out); } /// Helper for calling [allVariablesWithVisibility] to fetch all public variables. List> get allPublicVariables { return allVariablesWithVisibility(Visibility.public); } /// Use [allVariablesWithVisibility] instead. @Deprecated("allVariablesWithVisibility") List> 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> allVariablesWithVisibility(Visibility visibility) { var distinct = []; var out = >[]; void crawl(SymbolTable 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>.unmodifiable(out); } Variable? 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 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 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._(name, this, value: value); if (constant == true) v.lock(); _variables.add(v); return v; } /// Use [assign] instead. @Deprecated("assign") Variable 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 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? remove(String name) { SymbolTable? 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? 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 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 createChild({Map 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 clone() { var table = SymbolTable(); 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 fork({Map values = const {}}) { var table = SymbolTable(); table .._depth = _depth .._parent = _parent .._root = _root; table._variables.addAll(_variables.map((Variable v) { var variable = Variable._(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'; } }