From cb8630745f0773e17e859319b7555da0a2fd6f1f Mon Sep 17 00:00:00 2001 From: David Pearce Date: Mon, 9 Dec 2024 21:41:22 +1300 Subject: [PATCH] 412 implement defalias (#420) This adds support for `defalias`, along with various valid and invalid tests. The algorithm for resolving aliases requires a fixpoint iteration to allow for aliases of aliases, etc. This also fixes issues related to parsing of invalid identifiers (e.g. for constraint handles, column names, aliases, etc). --- pkg/corset/ast.go | 56 +++++++++++++++++++++++++++++++ pkg/corset/parser.go | 37 ++++++++++++++++++++- pkg/corset/resolver.go | 59 +++++++++++++++++++++++++++++++-- pkg/corset/scope.go | 31 +++++++++++++++++ pkg/corset/translator.go | 4 ++- pkg/test/invalid_corset_test.go | 31 +++++++++++++++++ pkg/test/valid_corset_test.go | 22 ++++++++++++ testdata/alias_02.accepts | 7 ++++ testdata/alias_02.lisp | 13 ++++++++ testdata/alias_02.rejects | 6 ++++ testdata/alias_03.accepts | 11 ++++++ testdata/alias_03.lisp | 18 ++++++++++ testdata/alias_03.rejects | 7 ++++ testdata/alias_04.accepts | 3 ++ testdata/alias_04.lisp | 4 +++ testdata/alias_05.accepts | 3 ++ testdata/alias_05.lisp | 4 +++ testdata/alias_06.accepts | 3 ++ testdata/alias_06.lisp | 5 +++ testdata/alias_invalid_01.lisp | 1 + testdata/alias_invalid_02.lisp | 4 +++ testdata/alias_invalid_03.lisp | 2 ++ testdata/alias_invalid_04.lisp | 2 ++ testdata/alias_invalid_05.lisp | 1 + testdata/alias_invalid_06.lisp | 2 ++ testdata/alias_invalid_07.lisp | 3 ++ 26 files changed, 335 insertions(+), 4 deletions(-) create mode 100644 testdata/alias_02.accepts create mode 100644 testdata/alias_02.lisp create mode 100644 testdata/alias_02.rejects create mode 100644 testdata/alias_03.accepts create mode 100644 testdata/alias_03.lisp create mode 100644 testdata/alias_03.rejects create mode 100644 testdata/alias_04.accepts create mode 100644 testdata/alias_04.lisp create mode 100644 testdata/alias_05.accepts create mode 100644 testdata/alias_05.lisp create mode 100644 testdata/alias_06.accepts create mode 100644 testdata/alias_06.lisp create mode 100644 testdata/alias_invalid_01.lisp create mode 100644 testdata/alias_invalid_02.lisp create mode 100644 testdata/alias_invalid_03.lisp create mode 100644 testdata/alias_invalid_04.lisp create mode 100644 testdata/alias_invalid_05.lisp create mode 100644 testdata/alias_invalid_06.lisp create mode 100644 testdata/alias_invalid_07.lisp diff --git a/pkg/corset/ast.go b/pkg/corset/ast.go index 46d921d..3bc44b0 100644 --- a/pkg/corset/ast.go +++ b/pkg/corset/ast.go @@ -174,6 +174,62 @@ func (e *ColumnName) Lisp() sexp.SExp { return sexp.NewSymbol(e.name) } +// ============================================================================ +// defalias +// ============================================================================ + +// DefAliases represents the declaration of one or more aliases. That is, +// alternate names for existing symbols. +type DefAliases struct { + // Aliases + aliases []*DefAlias + // Symbols being aliased + symbols []Symbol +} + +// Dependencies needed to signal declaration. +func (p *DefAliases) Dependencies() util.Iterator[Symbol] { + return util.NewArrayIterator[Symbol](nil) +} + +// Definitions returns the set of symbols defined by this declaration. Observe +// that these may not yet have been finalised. +func (p *DefAliases) Definitions() util.Iterator[SymbolDefinition] { + return util.NewArrayIterator[SymbolDefinition](nil) +} + +// Lisp converts this node into its lisp representation. This is primarily used +// for debugging purposes. +// +//nolint:revive +func (p *DefAliases) Lisp() sexp.SExp { + pairs := sexp.EmptyList() + // + for i, a := range p.aliases { + pairs.Append(sexp.NewSymbol(a.name)) + pairs.Append(p.symbols[i].Lisp()) + } + // + return sexp.NewList([]sexp.SExp{ + sexp.NewSymbol("defalias"), + pairs, + }) +} + +// DefAlias provides a node on which to hang source information to an alias name. +type DefAlias struct { + // Name of the alias + name string +} + +// Lisp converts this node into its lisp representation. This is primarily used +// for debugging purposes. +// +//nolint:revive +func (p *DefAlias) Lisp() sexp.SExp { + return sexp.NewSymbol(p.name) +} + // ============================================================================ // defcolumns // ============================================================================ diff --git a/pkg/corset/parser.go b/pkg/corset/parser.go index 8ee2606..2f8e326 100644 --- a/pkg/corset/parser.go +++ b/pkg/corset/parser.go @@ -233,7 +233,9 @@ func (p *Parser) parseDeclaration(module string, s *sexp.List) (Declaration, []S err *SyntaxError ) // - if s.MatchSymbols(1, "defcolumns") { + if s.MatchSymbols(1, "defalias") { + decl, errors = p.parseDefAlias(s.Elements) + } else if s.MatchSymbols(1, "defcolumns") { decl, errors = p.parseDefColumns(module, s) } else if s.Len() > 1 && s.MatchSymbols(1, "defconst") { decl, errors = p.parseDefConst(s.Elements) @@ -266,6 +268,39 @@ func (p *Parser) parseDeclaration(module string, s *sexp.List) (Declaration, []S return decl, errors } +// Parse an alias declaration +func (p *Parser) parseDefAlias(elements []sexp.SExp) (Declaration, []SyntaxError) { + var ( + errors []SyntaxError + aliases []*DefAlias + names []Symbol + ) + + for i := 1; i < len(elements); i += 2 { + // Sanity check first + if i+1 == len(elements) { + // Uneven number of constant declarations! + errors = append(errors, *p.translator.SyntaxError(elements[i], "missing alias definition")) + } else if !isIdentifier(elements[i]) { + // Symbol expected! + errors = append(errors, *p.translator.SyntaxError(elements[i], "invalid alias name")) + } else if !isIdentifier(elements[i+1]) { + // Symbol expected! + errors = append(errors, *p.translator.SyntaxError(elements[i+1], "invalid alias definition")) + } else { + alias := &DefAlias{elements[i].AsSymbol().Value} + name := &ColumnName{elements[i+1].AsSymbol().Value, nil} + p.mapSourceNode(elements[i], alias) + p.mapSourceNode(elements[i+1], name) + // + aliases = append(aliases, alias) + names = append(names, name) + } + } + // Done + return &DefAliases{aliases, names}, errors +} + // Parse a column declaration func (p *Parser) parseDefColumns(module string, l *sexp.List) (Declaration, []SyntaxError) { columns := make([]*DefColumn, l.Len()-1) diff --git a/pkg/corset/resolver.go b/pkg/corset/resolver.go index 6cddc55..b0b8096 100644 --- a/pkg/corset/resolver.go +++ b/pkg/corset/resolver.go @@ -67,10 +67,15 @@ func (r *resolver) resolveDeclarations(scope *GlobalScope, circuit *Circuit) []S // to process all columns before we can sure that they are all declared // correctly. func (r *resolver) resolveDeclarationsInModule(scope *ModuleScope, decls []Declaration) []SyntaxError { + // Columns & Assignments if errors := r.initialiseDeclarationsInModule(scope, decls); len(errors) > 0 { return errors } - // Iterate until all columns finalised + // Aliases + if errors := r.initialiseAliasesInModule(scope, decls); len(errors) > 0 { + return errors + } + // Finalise everything return r.finaliseDeclarationsInModule(scope, decls) } @@ -83,7 +88,7 @@ func (r *resolver) resolveDeclarationsInModule(scope *ModuleScope, decls []Decla func (r *resolver) initialiseDeclarationsInModule(scope *ModuleScope, decls []Declaration) []SyntaxError { module := scope.EnclosingModule() errors := make([]SyntaxError, 0) - // + // Initialise all columns for _, d := range decls { for iter := d.Definitions(); iter.HasNext(); { def := iter.Next() @@ -95,6 +100,56 @@ func (r *resolver) initialiseDeclarationsInModule(scope *ModuleScope, decls []De } } } + // + return errors +} + +// Initialise all alias declarations in the given module scope. This means +// declaring them within the module scope, whilst also supporting aliases of +// aliases, etc. Since the order of aliases is unspecified, this means we have +// to iterate the alias declarations until a fixed point is reached. Once that +// is done, if there are any aliases left unallocated then they indicate errors. +func (r *resolver) initialiseAliasesInModule(scope *ModuleScope, decls []Declaration) []SyntaxError { + // Apply any aliases + errors := make([]SyntaxError, 0) + visited := make(map[string]Declaration) + changed := true + // Iterate aliases to fixed point (i.e. until no new aliases discovered) + for changed { + changed = false + // Look for all aliases + for _, d := range decls { + if a, ok := d.(*DefAliases); ok { + for i, alias := range a.aliases { + symbol := a.symbols[i] + if _, ok := visited[alias.name]; !ok { + if change := scope.Alias(alias.name, symbol); change { + visited[alias.name] = d + changed = true + } + } + } + } + } + } + // Check for any aliases which remain incomplete + for _, decl := range decls { + if a, ok := decl.(*DefAliases); ok { + for i, alias := range a.aliases { + symbol := a.symbols[i] + // Check whether it already exists (or not) + if d, ok := visited[alias.name]; ok && d == decl { + continue + } else if scope.Binding(alias.name) != nil { + err := r.srcmap.SyntaxError(alias, "symbol already exists") + errors = append(errors, *err) + } else { + err := r.srcmap.SyntaxError(symbol, "unknown symbol") + errors = append(errors, *err) + } + } + } + } // Done return errors } diff --git a/pkg/corset/scope.go b/pkg/corset/scope.go index fab5c1f..4495bbc 100644 --- a/pkg/corset/scope.go +++ b/pkg/corset/scope.go @@ -141,6 +141,17 @@ func (p *ModuleScope) Bind(symbol Symbol) bool { return false } +// Binding returns information about the binding of a particular symbol defined +// in this module. +func (p *ModuleScope) Binding(name string) Binding { + // construct binding identifier + if bid, ok := p.ids[BindingId{name, false}]; ok { + return p.bindings[bid] + } + // Failure + return nil +} + // Column returns information about a particular column declared within this // module. func (p *ModuleScope) Column(name string) *ColumnBinding { @@ -167,6 +178,26 @@ func (p *ModuleScope) Declare(symbol SymbolDefinition) bool { return true } +// Alias constructs an alias for an existing symbol. If the symbol does not +// exist, then this returns false. +func (p *ModuleScope) Alias(alias string, symbol Symbol) bool { + // construct symbol identifier + symbol_id := BindingId{symbol.Name(), symbol.IsFunction()} + // construct alias identifier + alias_id := BindingId{alias, symbol.IsFunction()} + // Check alias does not already exist + if _, ok := p.ids[alias_id]; !ok { + // Check symbol being aliased exists + if id, ok := p.ids[symbol_id]; ok { + p.ids[alias_id] = id + // Done + return true + } + } + // Symbol not known (yet) + return false +} + // ============================================================================= // Local Scope // ============================================================================= diff --git a/pkg/corset/translator.go b/pkg/corset/translator.go index 87e7a6b..2e7cf61 100644 --- a/pkg/corset/translator.go +++ b/pkg/corset/translator.go @@ -138,7 +138,9 @@ func (t *translator) translateOtherDeclarationsInModule(module string, decls []D func (t *translator) translateDeclaration(decl Declaration, module string) []SyntaxError { var errors []SyntaxError // - if _, ok := decl.(*DefColumns); ok { + if _, ok := decl.(*DefAliases); ok { + // Not an assignment or a constraint, hence ignore. + } else if _, ok := decl.(*DefColumns); ok { // Not an assignment or a constraint, hence ignore. } else if _, ok := decl.(*DefConst); ok { // For now, constants are always compiled out when going down to HIR. diff --git a/pkg/test/invalid_corset_test.go b/pkg/test/invalid_corset_test.go index d544577..6d4a938 100644 --- a/pkg/test/invalid_corset_test.go +++ b/pkg/test/invalid_corset_test.go @@ -128,6 +128,37 @@ func Test_Invalid_Constant_14(t *testing.T) { CheckInvalid(t, "constant_invalid_14") } +// =================================================================== +// Alias Tests +// =================================================================== +func Test_Invalid_Alias_01(t *testing.T) { + CheckInvalid(t, "alias_invalid_01") +} + +func Test_Invalid_Alias_02(t *testing.T) { + CheckInvalid(t, "alias_invalid_02") +} + +func Test_Invalid_Alias_03(t *testing.T) { + CheckInvalid(t, "alias_invalid_03") +} + +func Test_Invalid_Alias_04(t *testing.T) { + CheckInvalid(t, "alias_invalid_04") +} + +func Test_Invalid_Alias_05(t *testing.T) { + CheckInvalid(t, "alias_invalid_05") +} + +func Test_Invalid_Alias_06(t *testing.T) { + CheckInvalid(t, "alias_invalid_06") +} + +func Test_Invalid_Alias_07(t *testing.T) { + CheckInvalid(t, "alias_invalid_07") +} + // =================================================================== // Property Tests // =================================================================== diff --git a/pkg/test/valid_corset_test.go b/pkg/test/valid_corset_test.go index 78503c8..91112a6 100644 --- a/pkg/test/valid_corset_test.go +++ b/pkg/test/valid_corset_test.go @@ -97,6 +97,28 @@ func Test_Constant_07(t *testing.T) { Check(t, "constant_07") } +// =================================================================== +// Alias Tests +// =================================================================== +func Test_Alias_01(t *testing.T) { + Check(t, "alias_01") +} +func Test_Alias_02(t *testing.T) { + Check(t, "alias_02") +} +func Test_Alias_03(t *testing.T) { + Check(t, "alias_03") +} +func Test_Alias_04(t *testing.T) { + Check(t, "alias_04") +} +func Test_Alias_05(t *testing.T) { + Check(t, "alias_05") +} +func Test_Alias_06(t *testing.T) { + Check(t, "alias_06") +} + // =================================================================== // Domain Tests // =================================================================== diff --git a/testdata/alias_02.accepts b/testdata/alias_02.accepts new file mode 100644 index 0000000..7382df9 --- /dev/null +++ b/testdata/alias_02.accepts @@ -0,0 +1,7 @@ +{"X": [], "Y": []} +{"X": [-1], "Y": [1]} +{"X": [0], "Y": [0]} +{"X": [1], "Y": [-1]} +{"X": [0,-1], "Y": [0,1]} +{"X": [0,0], "Y": [0,0]} +{"X": [0,1], "Y": [0,-1]} diff --git a/testdata/alias_02.lisp b/testdata/alias_02.lisp new file mode 100644 index 0000000..7c03669 --- /dev/null +++ b/testdata/alias_02.lisp @@ -0,0 +1,13 @@ +(defcolumns X Y) +(defalias + X' X + Y' Y) + +(defconstraint c1 () (+ X' Y')) +(defconstraint c2 () (+ Y' X')) +(defconstraint c3 () (+ X Y')) +(defconstraint c4 () (+ Y X')) +(defconstraint c5 () (+ X' Y)) +(defconstraint c6 () (+ Y' X)) +(defconstraint c7 () (+ X Y)) +(defconstraint c8 () (+ Y X)) diff --git a/testdata/alias_02.rejects b/testdata/alias_02.rejects new file mode 100644 index 0000000..cc18fc0 --- /dev/null +++ b/testdata/alias_02.rejects @@ -0,0 +1,6 @@ +{"X": [1], "Y": [0]} +{"X": [-1], "Y": [0]} +{"X": [0], "Y": [1]} +{"X": [0], "Y": [-1]} +{"X": [2], "Y": [-1]} +{"X": [-2], "Y": [1]} diff --git a/testdata/alias_03.accepts b/testdata/alias_03.accepts new file mode 100644 index 0000000..d86446d --- /dev/null +++ b/testdata/alias_03.accepts @@ -0,0 +1,11 @@ +{"X": [], "Y": []} +{"X": [-4], "Y": [2]} +{"X": [-2], "Y": [1]} +{"X": [0], "Y": [0]} +{"X": [2], "Y": [-1]} +{"X": [4], "Y": [-2]} +{"X": [0,-4], "Y": [0,2]} +{"X": [0,-2], "Y": [0,1]} +{"X": [0,0], "Y": [0,0]} +{"X": [0,2], "Y": [0,-1]} +{"X": [0,4], "Y": [0,-2]} diff --git a/testdata/alias_03.lisp b/testdata/alias_03.lisp new file mode 100644 index 0000000..8bb6933 --- /dev/null +++ b/testdata/alias_03.lisp @@ -0,0 +1,18 @@ +(defconst + ONE 1 + TWO 2 +) + +(defalias + one ONE + two TWO) + +(defcolumns X Y) +(defconstraint c1 () (+ X (* two Y))) +(defconstraint c2 () (+ (* two Y) X)) +(defconstraint c3 () (+ X Y Y)) +(defconstraint c4 () (+ Y X Y)) +(defconstraint c5 () (+ Y Y X)) +(defconstraint c6 () (+ (* one X) Y Y)) +(defconstraint c7 () (+ Y (* one X) Y)) +(defconstraint c8 () (+ Y Y (* one X))) diff --git a/testdata/alias_03.rejects b/testdata/alias_03.rejects new file mode 100644 index 0000000..c100014 --- /dev/null +++ b/testdata/alias_03.rejects @@ -0,0 +1,7 @@ +{"X": [-2], "Y": [-2]} +{"X": [-2], "Y": [-1]} +{"X": [-2], "Y": [0]} +{"X": [-2], "Y": [2]} +{"X": [-2], "Y": [3]} +{"X": [0,1], "Y": [0,1]} +{"X": [-2,1], "Y": [1,1]} diff --git a/testdata/alias_04.accepts b/testdata/alias_04.accepts new file mode 100644 index 0000000..5d2d6a2 --- /dev/null +++ b/testdata/alias_04.accepts @@ -0,0 +1,3 @@ +{"COUNTER": [0]} +{"COUNTER": [0,0]} +{"COUNTER": [0,0,0]} diff --git a/testdata/alias_04.lisp b/testdata/alias_04.lisp new file mode 100644 index 0000000..6ff2673 --- /dev/null +++ b/testdata/alias_04.lisp @@ -0,0 +1,4 @@ +(defcolumns COUNTER) +(defalias CT1 COUNTER) +(defalias CT2 CT1) +(defconstraint heartbeat () CT2) diff --git a/testdata/alias_05.accepts b/testdata/alias_05.accepts new file mode 100644 index 0000000..5d2d6a2 --- /dev/null +++ b/testdata/alias_05.accepts @@ -0,0 +1,3 @@ +{"COUNTER": [0]} +{"COUNTER": [0,0]} +{"COUNTER": [0,0,0]} diff --git a/testdata/alias_05.lisp b/testdata/alias_05.lisp new file mode 100644 index 0000000..42c5a33 --- /dev/null +++ b/testdata/alias_05.lisp @@ -0,0 +1,4 @@ +(defcolumns COUNTER) +(defalias CT2 CT1) +(defalias CT1 COUNTER) +(defconstraint heartbeat () CT2) diff --git a/testdata/alias_06.accepts b/testdata/alias_06.accepts new file mode 100644 index 0000000..5d2d6a2 --- /dev/null +++ b/testdata/alias_06.accepts @@ -0,0 +1,3 @@ +{"COUNTER": [0]} +{"COUNTER": [0,0]} +{"COUNTER": [0,0,0]} diff --git a/testdata/alias_06.lisp b/testdata/alias_06.lisp new file mode 100644 index 0000000..4c41cc4 --- /dev/null +++ b/testdata/alias_06.lisp @@ -0,0 +1,5 @@ +(defcolumns COUNTER) +(defalias CT3 CT2) +(defalias CT2 CT1) +(defalias CT1 COUNTER) +(defconstraint heartbeat () CT3) diff --git a/testdata/alias_invalid_01.lisp b/testdata/alias_invalid_01.lisp new file mode 100644 index 0000000..2ed0c17 --- /dev/null +++ b/testdata/alias_invalid_01.lisp @@ -0,0 +1 @@ +(defalias X Y) diff --git a/testdata/alias_invalid_02.lisp b/testdata/alias_invalid_02.lisp new file mode 100644 index 0000000..56e73e3 --- /dev/null +++ b/testdata/alias_invalid_02.lisp @@ -0,0 +1,4 @@ +(defcolumns Y) +(defalias + X Y + A B) diff --git a/testdata/alias_invalid_03.lisp b/testdata/alias_invalid_03.lisp new file mode 100644 index 0000000..ec8ca7c --- /dev/null +++ b/testdata/alias_invalid_03.lisp @@ -0,0 +1,2 @@ +(defcolumns X Y) +(defalias X Y) diff --git a/testdata/alias_invalid_04.lisp b/testdata/alias_invalid_04.lisp new file mode 100644 index 0000000..e3027bb --- /dev/null +++ b/testdata/alias_invalid_04.lisp @@ -0,0 +1,2 @@ +(defpurefun (id x) x) +(defalias fn id) diff --git a/testdata/alias_invalid_05.lisp b/testdata/alias_invalid_05.lisp new file mode 100644 index 0000000..1b7447f --- /dev/null +++ b/testdata/alias_invalid_05.lisp @@ -0,0 +1 @@ +(defalias x x) diff --git a/testdata/alias_invalid_06.lisp b/testdata/alias_invalid_06.lisp new file mode 100644 index 0000000..1a3369e --- /dev/null +++ b/testdata/alias_invalid_06.lisp @@ -0,0 +1,2 @@ +(defalias x y) +(defalias y x) diff --git a/testdata/alias_invalid_07.lisp b/testdata/alias_invalid_07.lisp new file mode 100644 index 0000000..7f0eb8e --- /dev/null +++ b/testdata/alias_invalid_07.lisp @@ -0,0 +1,3 @@ +(defcolumns X Y) +(defalias CT X) +(defalias CT Y)