From 4c9bc1c9be1d8a7e097d6a42aa9f94288d312ab5 Mon Sep 17 00:00:00 2001 From: thomashii Date: Sat, 11 Sep 2021 22:09:54 +0800 Subject: [PATCH] Added merge_map and symbol_table --- packages/merge_map/AUTHORS.md | 12 + packages/merge_map/CHANGELOG.md | 16 + packages/merge_map/LICENSE | 21 ++ packages/merge_map/README.md | 25 ++ packages/merge_map/analysis_options.yaml | 4 + packages/merge_map/example/main.dart | 20 ++ packages/merge_map/lib/angel3_merge_map.dart | 34 ++ packages/merge_map/pubspec.yaml | 9 + packages/merge_map/test/all_test.dart | 104 ++++++ packages/symbol_table/AUTHORS.md | 12 + packages/symbol_table/CHANGELOG.md | 26 ++ packages/symbol_table/LICENSE | 21 ++ packages/symbol_table/README.md | 161 +++++++++ packages/symbol_table/analysis_options.yaml | 4 + packages/symbol_table/example/main.dart | 26 ++ .../symbol_table/lib/angel3_symbol_table.dart | 1 + .../symbol_table/lib/src/symbol_table.dart | 320 ++++++++++++++++++ packages/symbol_table/lib/src/variable.dart | 51 +++ packages/symbol_table/lib/src/visibility.dart | 31 ++ packages/symbol_table/pubspec.yaml | 11 + packages/symbol_table/symbol_table.iml | 14 + packages/symbol_table/test/all_test.dart | 144 ++++++++ 22 files changed, 1067 insertions(+) create mode 100644 packages/merge_map/AUTHORS.md create mode 100644 packages/merge_map/CHANGELOG.md create mode 100644 packages/merge_map/LICENSE create mode 100644 packages/merge_map/README.md create mode 100644 packages/merge_map/analysis_options.yaml create mode 100644 packages/merge_map/example/main.dart create mode 100644 packages/merge_map/lib/angel3_merge_map.dart create mode 100644 packages/merge_map/pubspec.yaml create mode 100644 packages/merge_map/test/all_test.dart create mode 100644 packages/symbol_table/AUTHORS.md create mode 100644 packages/symbol_table/CHANGELOG.md create mode 100644 packages/symbol_table/LICENSE create mode 100644 packages/symbol_table/README.md create mode 100644 packages/symbol_table/analysis_options.yaml create mode 100644 packages/symbol_table/example/main.dart create mode 100644 packages/symbol_table/lib/angel3_symbol_table.dart create mode 100644 packages/symbol_table/lib/src/symbol_table.dart create mode 100644 packages/symbol_table/lib/src/variable.dart create mode 100644 packages/symbol_table/lib/src/visibility.dart create mode 100644 packages/symbol_table/pubspec.yaml create mode 100644 packages/symbol_table/symbol_table.iml create mode 100644 packages/symbol_table/test/all_test.dart diff --git a/packages/merge_map/AUTHORS.md b/packages/merge_map/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/packages/merge_map/AUTHORS.md @@ -0,0 +1,12 @@ +Primary Authors +=============== + +* __[Thomas Hii](dukefirehawk.apps@gmail.com)__ + + Thomas is the current maintainer of the code base. He has refactored and migrated the + code base to support NNBD. + +* __[Tobe O](thosakwe@gmail.com)__ + + Tobe has written much of the original code prior to NNBD migration. He has moved on and + is no longer involved with the project. diff --git a/packages/merge_map/CHANGELOG.md b/packages/merge_map/CHANGELOG.md new file mode 100644 index 0000000..31f362c --- /dev/null +++ b/packages/merge_map/CHANGELOG.md @@ -0,0 +1,16 @@ +# 2.0.2 +* Resolve static analysis warnings + +# 2.0.1 +* Updated README + +# 2.0.0 +* Migrated to work with Dart SDK 2.12.x NNBD + +# 1.0.2 +* Add an example, for Pub's sake. + +# 1.0.1 +* Add a specific constraint on Dart versions, to prevent Pub from rejecting all packages that depend on +`merge_map` (the entire Angel framework). +* Add generic type support diff --git a/packages/merge_map/LICENSE b/packages/merge_map/LICENSE new file mode 100644 index 0000000..8f65b57 --- /dev/null +++ b/packages/merge_map/LICENSE @@ -0,0 +1,21 @@ +MIT License (MIT) + +Copyright (c) 2021 dukefirehawk.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/merge_map/README.md b/packages/merge_map/README.md new file mode 100644 index 0000000..21a94bb --- /dev/null +++ b/packages/merge_map/README.md @@ -0,0 +1,25 @@ +# angel3_merge_map +[![version](https://img.shields.io/badge/pub-v2.0.2-brightgreen)](https://pub.dartlang.org/packages/angel3_merge_map) +[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) +[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) + +[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/merge_map/LICENSE) + +Combine multiple Maps into one. Equivalent to +[Object.assign](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign) +in JS. + +# Example + +```dart +import "package:angel3_merge_map/angel3_merge_map.dart"; + +void main() { + Map map1 = {'hello': 'world'}; + Map map2 = {'foo': {'bar': 'baz', 'this': 'will be overwritten'}}; + Map map3 = {'foo': {'john': 'doe', 'this': 'overrides previous maps'}}; + Map merged = mergeMap(map1, map2, map3); + + // {hello: world, foo: {bar: baz, john: doe, this: overrides previous maps}} +} +``` \ No newline at end of file diff --git a/packages/merge_map/analysis_options.yaml b/packages/merge_map/analysis_options.yaml new file mode 100644 index 0000000..c230cee --- /dev/null +++ b/packages/merge_map/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/packages/merge_map/example/main.dart b/packages/merge_map/example/main.dart new file mode 100644 index 0000000..c3b7c46 --- /dev/null +++ b/packages/merge_map/example/main.dart @@ -0,0 +1,20 @@ +import 'package:angel3_merge_map/angel3_merge_map.dart'; + +void main() { + // ignore: omit_local_variable_types + Map map1 = {'hello': 'world'}; + + // ignore: omit_local_variable_types + Map map2 = { + 'foo': {'bar': 'baz', 'this': 'will be overwritten'} + }; + + // ignore: omit_local_variable_types + Map map3 = { + 'foo': {'john': 'doe', 'this': 'overrides previous maps'} + }; + var merged = mergeMap([map1, map2, map3]); + print(merged); + + // {hello: world, foo: {bar: baz, john: doe, this: overrides previous maps}} +} diff --git a/packages/merge_map/lib/angel3_merge_map.dart b/packages/merge_map/lib/angel3_merge_map.dart new file mode 100644 index 0000000..8158489 --- /dev/null +++ b/packages/merge_map/lib/angel3_merge_map.dart @@ -0,0 +1,34 @@ +/// Exposes the [mergeMap] function, which... merges Maps. +library angel3_merge_map; + +dynamic _copyValues( + Map from, Map? to, bool recursive, bool acceptNull) { + for (var key in from.keys) { + if (from[key] is Map && recursive) { + if (!(to![key] is Map)) { + to[key] = {} as V; + } + _copyValues(from[key] as Map, to[key] as Map?, recursive, acceptNull); + } else { + if (from[key] != null || acceptNull) to![key] = from[key]; + } + } +} + +/// Merges the values of the given maps together. +/// +/// `recursive` is set to `true` by default. If set to `true`, +/// then nested maps will also be merged. Otherwise, nested maps +/// will overwrite others. +/// +/// `acceptNull` is set to `false` by default. If set to `false`, +/// then if the value on a map is `null`, it will be ignored, and +/// that `null` will not be copied. +Map mergeMap(Iterable> maps, + {bool recursive = true, bool acceptNull = false}) { + var result = {}; + maps.forEach((Map map) { + _copyValues(map, result, recursive, acceptNull); + }); + return result; +} diff --git a/packages/merge_map/pubspec.yaml b/packages/merge_map/pubspec.yaml new file mode 100644 index 0000000..a4412aa --- /dev/null +++ b/packages/merge_map/pubspec.yaml @@ -0,0 +1,9 @@ +name: angel3_merge_map +version: 2.0.2 +description: Combine multiple Maps into one. Equivalent to Object.assign in JS. +homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/merge_map +environment: + sdk: '>=2.12.0 <3.0.0' +dev_dependencies: + test: ^1.17.4 + pedantic: ^1.11.0 diff --git a/packages/merge_map/test/all_test.dart b/packages/merge_map/test/all_test.dart new file mode 100644 index 0000000..06ab569 --- /dev/null +++ b/packages/merge_map/test/all_test.dart @@ -0,0 +1,104 @@ +import 'package:angel3_merge_map/angel3_merge_map.dart'; +import 'package:test/test.dart'; + +void main() { + test('can merge two simple maps', () { + var merged = mergeMap([ + {'hello': 'world'}, + {'hello': 'dolly'} + ]); + expect(merged['hello'], equals('dolly')); + }); + + test("the last map's values supersede those of prior", () { + var merged = mergeMap([ + {'letter': 'a'}, + {'letter': 'b'}, + {'letter': 'c'} + ]); + expect(merged['letter'], equals('c')); + }); + + test('can merge two once-nested maps', () { + // ignore: omit_local_variable_types + Map map1 = { + 'hello': 'world', + 'foo': {'nested': false} + }; + // ignore: omit_local_variable_types + Map map2 = { + 'goodbye': 'sad life', + 'foo': {'nested': true, 'it': 'works'} + }; + var merged = mergeMap([map1, map2]); + + expect(merged['hello'], equals('world')); + expect(merged['goodbye'], equals('sad life')); + expect(merged['foo']['nested'], equals(true)); + expect(merged['foo']['it'], equals('works')); + }); + + test('once-nested map supersession', () { + // ignore: omit_local_variable_types + Map map1 = { + 'hello': 'world', + 'foo': {'nested': false} + }; + // ignore: omit_local_variable_types + Map map2 = { + 'goodbye': 'sad life', + 'foo': {'nested': true, 'it': 'works'} + }; + // ignore: omit_local_variable_types + Map map3 = { + 'foo': {'nested': 'supersession'} + }; + + var merged = mergeMap([map1, map2, map3]); + expect(merged['foo']['nested'], equals('supersession')); + }); + + test('can merge two twice-nested maps', () { + // ignore: omit_local_variable_types + Map map1 = { + 'a': { + 'b': {'c': 'd'} + } + }; + // ignore: omit_local_variable_types + Map map2 = { + 'a': { + 'b': {'c': 'D', 'e': 'f'} + } + }; + var merged = mergeMap([map1, map2]); + + expect(merged['a']['b']['c'], equals('D')); + expect(merged['a']['b']['e'], equals('f')); + }); + + test('twice-nested map supersession', () { + // ignore: omit_local_variable_types + Map map1 = { + 'a': { + 'b': {'c': 'd'} + } + }; + // ignore: omit_local_variable_types + Map map2 = { + 'a': { + 'b': {'c': 'D', 'e': 'f'} + } + }; + // ignore: omit_local_variable_types + Map map3 = { + 'a': { + 'b': {'e': 'supersession'} + } + }; + var merged = mergeMap([map1, map2, map3]); + + expect(merged['a']['b']['c'], equals('D')); + expect(merged['a']['b']['e'], equals('supersession')); + }); +} diff --git a/packages/symbol_table/AUTHORS.md b/packages/symbol_table/AUTHORS.md new file mode 100644 index 0000000..ac95ab5 --- /dev/null +++ b/packages/symbol_table/AUTHORS.md @@ -0,0 +1,12 @@ +Primary Authors +=============== + +* __[Thomas Hii](dukefirehawk.apps@gmail.com)__ + + Thomas is the current maintainer of the code base. He has refactored and migrated the + code base to support NNBD. + +* __[Tobe O](thosakwe@gmail.com)__ + + Tobe has written much of the original code prior to NNBD migration. He has moved on and + is no longer involved with the project. diff --git a/packages/symbol_table/CHANGELOG.md b/packages/symbol_table/CHANGELOG.md new file mode 100644 index 0000000..9dd305e --- /dev/null +++ b/packages/symbol_table/CHANGELOG.md @@ -0,0 +1,26 @@ +# 2.0.2 +* Resolved static analysis warnings + +# 2.0.1 +* Resolved static analysis warnings + +# 2.0.0 +* Migrated to work with Dart SDK 2.12.x NNBD + +## 1.0.4 +* Added `context` to `SymbolTable`. + +## 1.0.3 +* Converted `Visibility` into a `Comparable` class. +* Renamed `add` -> `create`, `put` -> `assign`, and `allVariablesOfVisibility` -> `allVariablesWithVisibility`. +* Added tests for `Visibility` comparing, and `depth`. +* Added `uniqueName()` to `SymbolTable`. +* Fixed a typo in `remove` that would have prevented it from working correctly. + +## 1.0.2 +* Added `depth` to `SymbolTable`. +* Added `symbolTable` to `Variable`. +* Deprecated the redundant `Constant` class. +* Deprecated `Variable.markAsPrivate()`. +* Added the `Visibility` enumerator. +* Added the field `visibility` to `Variable`. \ No newline at end of file diff --git a/packages/symbol_table/LICENSE b/packages/symbol_table/LICENSE new file mode 100644 index 0000000..8f65b57 --- /dev/null +++ b/packages/symbol_table/LICENSE @@ -0,0 +1,21 @@ +MIT License (MIT) + +Copyright (c) 2021 dukefirehawk.com + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/packages/symbol_table/README.md b/packages/symbol_table/README.md new file mode 100644 index 0000000..364ee10 --- /dev/null +++ b/packages/symbol_table/README.md @@ -0,0 +1,161 @@ +# angel3_symbol_table +[![version](https://img.shields.io/badge/pub-v2.0.2-brightgreen)](https://pub.dartlang.org/packages/angel3_symbol_table) +[![Null Safety](https://img.shields.io/badge/null-safety-brightgreen)](https://dart.dev/null-safety) +[![Gitter](https://img.shields.io/gitter/room/angel_dart/discussion)](https://gitter.im/angel_dart/discussion) + +[![License](https://img.shields.io/github/license/dukefirehawk/angel)](https://github.com/dukefirehawk/angel/tree/angel3/packages/symbol_table/LICENSE) + +A generic symbol table implementation in Dart, with support for scopes and constants. +The symbol tables produced by this package are hierarchical (in this case, tree-shaped), +and utilize basic memoization to speed up repeated lookups. + +# Variables +To represent a symbol, use `Variable`. I opted for the name +`Variable` to avoid conflict with the Dart primitive `Symbol`. + +```dart +var foo = Variable('foo'); +var bar = Variable('bar', value: 'baz'); + +// Call `lock` to mark a symbol as immutable. +var shelley = Variable('foo', value: 'bar')..lock(); + +foo.value = 'bar'; +shelley.value = 'Mary'; // Throws a StateError - constants cannot be overwritten. + +foo.lock(); +foo.value = 'baz'; // Also throws a StateError - Once a variable is locked, it cannot be overwritten. +``` + +## Visibility +Variables are *public* by default, but can also be marked as *private* or *protected*. This can be helpful if you are trying +to determine which symbols should be exported from a library or class. + +```dart +myVariable.visibility = Visibility.protected; +myVariable.visibility = Visibility.private; +``` + +# Symbol Tables +It's easy to create a basic symbol table: + +```dart +var mySymbolTable = SymbolTable(); +var doubles = SymbolTable(values: { + 'hydrogen': 1.0, + 'avogadro': 6.022e23 +}); + +// Create a new variable within the scope. +doubles.create('one'); +doubles.create('one', value: 1.0); +doubles.create('one', value: 1.0, constant: true); + +// Set a variable within an ancestor, OR create a new variable if none exists. +doubles.assign('two', 2.0); + +// Completely remove a variable. +doubles.remove('two'); + +// Find a symbol, either in this symbol table or an ancestor. +var symbol = doubles.resolve('one'); + +// Find OR create a symbol. +var symbol = doubles.resolveOrCreate('one'); +var symbol = doubles.resolveOrCreate('one', value: 1.0); +var symbol = doubles.resolveOrCreate('one', value: 1.0, constant: true); +``` + +# Exporting Symbols +Due to the tree structure of symbol tables, it is extremely easy to +extract a linear list of distinct variables, with variables lower in the hierarchy superseding their parents +(effectively accomplishing variable shadowing). + +```dart +var allSymbols = mySymbolTable.allVariables; +``` + +We can also extract symbols which are *not* private. This helps us export symbols from libraries +or classes. + +```dart +var exportedSymbols = mySymbolTable.allPublicVariables; +``` + +It's easy to extract symbols of a given visibility: +```dart +var exportedSymbols = mySymbolTable.allVariablesWithVisibility(Visibility.protected); +``` + +# Child Scopes +There are three ways to create a new symbol table: + + +## Regular Children +This is what most interpreters need; it simply creates a symbol table with the current symbol table +as its parent. The new scope can define its own symbols, which will only shadow the ancestors within the +correct scope. + +```dart +var child = mySymbolTable.createChild(); +var child = mySymbolTable.createChild(values: {...}); +``` + +### Depth +Every symbol table has an associated `depth` attached to it, with the `depth` at the root +being `0`. When `createChild` is called, the resulting child has an incremented `depth`. + +## Clones +This creates a scope at the same level as the current one, with all the same variables. + +```dart +var clone = mySymbolTable.clone(); +``` + +## Forked Scopes +If you are implementing a language with closure functions, you might consider looking into this. +A forked scope is a scope identical to the current one, but instead of merely copying references +to variables, the values of variables are copied into new ones. + +The new scope is essentially a "frozen" version of the current one. + +It is also effectively orphaned - though it is aware of its `parent`, the parent scope is unaware +that the forked scope is a child. Thus, calls to `resolve` may return old variables, if a parent +has called `remove` on a symbol. + +```dart +var forked = mySymbolTable.fork(); +var forked = mySymbolTable.fork(values: {...}); +``` + +# Creating Names +In languages with block scope, oftentimes, identifiers will collide within a global scope. +To avoid this, symbol tables expose a `uniqueName()` method that simply attaches a numerical suffix to +an input name. The name is guaranteed to never be repeated within a specific scope. + +```dart +var name0 = mySymbolTable.uniqueName('foo'); // foo0 +var name1 = mySymbolTable.uniqueName('foo'); // foo1 +var name2 = mySymbolTable.uniqueName('foo'); // foo2 +``` + +# `this` Context +Many languages handle a sort of `this` context that values within a scope may +optionally be resolved against. Symbol tables can easily set their context +as follows: + +```dart +void foo() { + mySymbolTable.context = thisContext; +} +``` + +Resolution of the `context` getter functions just like a symbol; if none is +set locally, then it will refer to the parent. + +```dart +void bar() { + mySymbolTable.context = thisContext; + expect(mySymbolTable.createChild().createChild().context, thisContext); +} +``` \ No newline at end of file diff --git a/packages/symbol_table/analysis_options.yaml b/packages/symbol_table/analysis_options.yaml new file mode 100644 index 0000000..c230cee --- /dev/null +++ b/packages/symbol_table/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:pedantic/analysis_options.yaml +analyzer: + strong-mode: + implicit-casts: false \ No newline at end of file diff --git a/packages/symbol_table/example/main.dart b/packages/symbol_table/example/main.dart new file mode 100644 index 0000000..5c804cf --- /dev/null +++ b/packages/symbol_table/example/main.dart @@ -0,0 +1,26 @@ +import 'package:angel3_symbol_table/angel3_symbol_table.dart'; + +void main(List args) { + //var mySymbolTable = SymbolTable(); + var doubles = + SymbolTable(values: {'hydrogen': 1.0, 'avogadro': 6.022e23}); + +// Create a new variable within the scope. + doubles.create('one'); + doubles.create('one', value: 1.0); + doubles.create('one', value: 1.0, constant: true); + +// Set a variable within an ancestor, OR create a new variable if none exists. + doubles.assign('two', 2.0); + +// Completely remove a variable. + doubles.remove('two'); + +// Find a symbol, either in this symbol table or an ancestor. + //var symbol1 = doubles.resolve('one'); + +// Find OR create a symbol. + //var symbol2 = doubles.resolveOrCreate('one'); + //var symbol3 = doubles.resolveOrCreate('one', value: 1.0); + //var symbol4 = doubles.resolveOrCreate('one', value: 1.0, constant: true); +} diff --git a/packages/symbol_table/lib/angel3_symbol_table.dart b/packages/symbol_table/lib/angel3_symbol_table.dart new file mode 100644 index 0000000..d945b7a --- /dev/null +++ b/packages/symbol_table/lib/angel3_symbol_table.dart @@ -0,0 +1 @@ +export 'src/symbol_table.dart'; diff --git a/packages/symbol_table/lib/src/symbol_table.dart b/packages/symbol_table/lib/src/symbol_table.dart new file mode 100644 index 0000000..40e3021 --- /dev/null +++ b/packages/symbol_table/lib/src/symbol_table.dart @@ -0,0 +1,320 @@ +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 + 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); + _children.forEach((c) => c._wipeLookupCache(key)); + } + + /// Use [create] instead. + @deprecated + 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 + 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'; + } +} diff --git a/packages/symbol_table/lib/src/variable.dart b/packages/symbol_table/lib/src/variable.dart new file mode 100644 index 0000000..2a648c6 --- /dev/null +++ b/packages/symbol_table/lib/src/variable.dart @@ -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 extends Variable { + 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 { + final String name; + final SymbolTable? 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; + } +} diff --git a/packages/symbol_table/lib/src/visibility.dart b/packages/symbol_table/lib/src/visibility.dart new file mode 100644 index 0000000..4144e0f --- /dev/null +++ b/packages/symbol_table/lib/src/visibility.dart @@ -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 { + 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); + } +} diff --git a/packages/symbol_table/pubspec.yaml b/packages/symbol_table/pubspec.yaml new file mode 100644 index 0000000..c777d4c --- /dev/null +++ b/packages/symbol_table/pubspec.yaml @@ -0,0 +1,11 @@ +name: angel3_symbol_table +version: 2.0.2 +description: A generic symbol table implementation in Dart, with support for scopes and constants. +homepage: https://github.com/dukefirehawk/angel/tree/angel3/packages/symbol_table +environment: + sdk: '>=2.12.0 <3.0.0' +dependencies: + collection: ^1.15.0 +dev_dependencies: + test: ^1.17.4 + pedantic: ^1.11.0 diff --git a/packages/symbol_table/symbol_table.iml b/packages/symbol_table/symbol_table.iml new file mode 100644 index 0000000..5a5ced2 --- /dev/null +++ b/packages/symbol_table/symbol_table.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/packages/symbol_table/test/all_test.dart b/packages/symbol_table/test/all_test.dart new file mode 100644 index 0000000..a126bc7 --- /dev/null +++ b/packages/symbol_table/test/all_test.dart @@ -0,0 +1,144 @@ +import 'package:angel3_symbol_table/angel3_symbol_table.dart'; +import 'package:test/test.dart'; + +void main() { + late SymbolTable scope; + + setUp(() { + scope = SymbolTable(values: {'one': 1}); + }); + + test('starter values', () { + expect(scope['one']?.value, 1); + }); + + test('add', () { + var two = scope.create('two', value: 2); + expect(two.value, 2); + expect(two.isImmutable, isFalse); + }); + + test('put', () { + var one = scope.resolve('one'); + var child = scope.createChild(); + var three = child.assign('one', 3); + expect(three.value, 3); + expect(three, one); + }); + + test('private', () { + var three = scope.create('three', value: 3) + ..visibility = Visibility.private; + expect(scope.allVariables, contains(three)); + expect( + scope.allVariablesWithVisibility(Visibility.private), contains(three)); + expect(scope.allPublicVariables, isNot(contains(three))); + }); + + test('protected', () { + var three = scope.create('three', value: 3) + ..visibility = Visibility.protected; + expect(scope.allVariables, contains(three)); + expect(scope.allVariablesWithVisibility(Visibility.protected), + contains(three)); + expect(scope.allPublicVariables, isNot(contains(three))); + }); + + test('constants', () { + var two = scope.create('two', value: 2, constant: true); + expect(two.value, 2); + expect(two.isImmutable, isTrue); + expect(() => scope['two'] = 3, throwsStateError); + }); + + test('lock', () { + expect(scope['one']?.isImmutable, isFalse); + scope['one']!.lock(); + expect(scope['one']?.isImmutable, isTrue); + expect(() => scope['one'] = 2, throwsStateError); + }); + + test('child', () { + expect(scope.createChild().createChild().resolve('one')!.value, 1); + }); + + test('clone', () { + var child = scope.createChild(); + var clone = child.clone(); + expect(clone.resolve('one'), child.resolve('one')); + expect(clone.parent, child.parent); + }); + + test('fork', () { + var fork = scope.fork(); + scope.assign('three', 3); + + expect(scope.resolve('three'), isNotNull); + expect(fork.resolve('three'), isNull); + }); + + test('remove', () { + var one = scope.remove('one')!; + expect(one.value, 1); + + expect(scope.resolve('one'), isNull); + }); + + test('root', () { + expect(scope.isRoot, isTrue); + expect(scope.root, scope); + + var child = scope + .createChild() + .createChild() + .createChild() + .createChild() + .createChild() + .createChild() + .createChild(); + expect(child.isRoot, false); + expect(child.root, scope); + }); + + test('visibility comparisons', () { + expect([Visibility.private, Visibility.protected], + everyElement(lessThan(Visibility.public))); + expect(Visibility.private, lessThan(Visibility.protected)); + expect(Visibility.protected, greaterThan(Visibility.private)); + expect(Visibility.public, greaterThan(Visibility.private)); + expect(Visibility.public, greaterThan(Visibility.protected)); + }); + + test('depth', () { + expect(scope.depth, 0); + expect(scope.clone().depth, 0); + expect(scope.fork().depth, 0); + expect(scope.createChild().depth, 1); + expect(scope.createChild().createChild().depth, 2); + expect(scope.createChild().createChild().createChild().depth, 3); + }); + + test('unique name', () { + expect(scope.uniqueName('foo'), 'foo0'); + expect(scope.uniqueName('foo'), 'foo1'); + expect(scope.createChild().uniqueName('foo'), 'foo2'); + expect(scope.createChild().uniqueName('foo'), 'foo2'); + var child = scope.createChild(); + expect(child.uniqueName('foo'), 'foo2'); + expect(child.uniqueName('foo'), 'foo3'); + expect(child.createChild().uniqueName('foo'), 'foo4'); + }); + + test('context', () { + scope.context = 24; + expect(scope.context, 24); + expect(scope.createChild().context, 24); + expect(scope.createChild().createChild().context, 24); + + var child = scope.createChild().createChild()..context = 35; + expect(child.context, 35); + expect(child.createChild().context, 35); + expect(child.createChild().createChild().context, 35); + expect(scope.context, 24); + }); +}