Skip to content

Commit

Permalink
412 implement defalias (#420)
Browse files Browse the repository at this point in the history
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).
  • Loading branch information
DavePearce authored Dec 9, 2024
1 parent 13af9a0 commit cb86307
Show file tree
Hide file tree
Showing 26 changed files with 335 additions and 4 deletions.
56 changes: 56 additions & 0 deletions pkg/corset/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ============================================================================
Expand Down
37 changes: 36 additions & 1 deletion pkg/corset/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
59 changes: 57 additions & 2 deletions pkg/corset/resolver.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand All @@ -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()
Expand All @@ -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
}
Expand Down
31 changes: 31 additions & 0 deletions pkg/corset/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
// =============================================================================
Expand Down
4 changes: 3 additions & 1 deletion pkg/corset/translator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
31 changes: 31 additions & 0 deletions pkg/test/invalid_corset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ===================================================================
Expand Down
22 changes: 22 additions & 0 deletions pkg/test/valid_corset_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
// ===================================================================
Expand Down
7 changes: 7 additions & 0 deletions testdata/alias_02.accepts
Original file line number Diff line number Diff line change
@@ -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]}
13 changes: 13 additions & 0 deletions testdata/alias_02.lisp
Original file line number Diff line number Diff line change
@@ -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))
6 changes: 6 additions & 0 deletions testdata/alias_02.rejects
Original file line number Diff line number Diff line change
@@ -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]}
11 changes: 11 additions & 0 deletions testdata/alias_03.accepts
Original file line number Diff line number Diff line change
@@ -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]}
18 changes: 18 additions & 0 deletions testdata/alias_03.lisp
Original file line number Diff line number Diff line change
@@ -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)))
7 changes: 7 additions & 0 deletions testdata/alias_03.rejects
Original file line number Diff line number Diff line change
@@ -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]}
3 changes: 3 additions & 0 deletions testdata/alias_04.accepts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{"COUNTER": [0]}
{"COUNTER": [0,0]}
{"COUNTER": [0,0,0]}
4 changes: 4 additions & 0 deletions testdata/alias_04.lisp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
(defcolumns COUNTER)
(defalias CT1 COUNTER)
(defalias CT2 CT1)
(defconstraint heartbeat () CT2)
Loading

0 comments on commit cb86307

Please sign in to comment.