Skip to content

Commit

Permalink
feat: support perspective name qualification (#504)
Browse files Browse the repository at this point in the history
* Add initial tests

This adds some initial tests for perspective qualified notation.

* use paths intead of explicit qualifiers

This updates the implementation to use the notion of a "path" rather
than a trio of (module, perspective, name).  This should future proof us
for recursive submodules, and also simply helps ensure things actually
make sense.

* Replace `GlobalScope` and `ModuleScope`

This replaces these two constructs with a single new construct (called,
helpfully, `ModuleScope`).  The difference is that the new construct can
describe nested trees, thus making it suitable for handling perspectives
(and later submodules).

At this stage, register allocation is now working again with
perspectives.  I've chosen to retain backwards compatibility in terms of
naming with the original corset.  The issue is around the naming of
registers which arise from coalescing perspective columns.  The names
currently do not include the perspective name, and this means they can
potentially clash.  However, clashing is not a critical issue at this
time, since it will always result in the generated Trace.java file
containing a syntax error.
  • Loading branch information
DavePearce authored Jan 8, 2025
1 parent 2da7e49 commit 7ab511b
Show file tree
Hide file tree
Showing 37 changed files with 1,412 additions and 544 deletions.
39 changes: 27 additions & 12 deletions pkg/corset/allocation.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,13 +77,11 @@ func (r *Register) Deactivate() {
// RegisterSource provides necessary information about source-level columns
// allocated to a given register.
type RegisterSource struct {
// Module in which source-level column resides
module string
// Perspective containing source-level column. If none, then this is
// assigned "".
perspective string
// Name of source-level column.
name string
// Context is a prefix of name which, when they differ, indicates a virtual
// column (i.e. one which is subject to register allocation).
context util.Path
// Fully qualified (i.e. absolute) name of source-level column.
name util.Path
// Length multiplier of source-level column.
multiplier uint
// Underlying datatype of the source-level column.
Expand All @@ -94,6 +92,19 @@ type RegisterSource struct {
computed bool
}

// IsVirtual indicates whether or not this is a "virtual" column. That is,
// something which is subject to register allocation (i.e. because it is
// declared in a perspective).
func (p *RegisterSource) IsVirtual() bool {
return !p.name.Parent().Equals(p.context)
}

// Perspective returns the name of the "virtual perspective" in which this
// column exists.
func (p *RegisterSource) Perspective() string {
return p.name.Parent().Slice(p.context.Depth()).String()
}

// RegisterAllocation is a generic interface to support different "regsiter
// allocation" algorithms. More specifically, register allocation is the
// process of allocating columns to their underlying HIR columns (a.k.a
Expand Down Expand Up @@ -186,20 +197,24 @@ func NewRegisterAllocator(allocation RegisterAllocation) *RegisterAllocator {
// Identify all perspectives
for iter := allocation.Registers(); iter.HasNext(); {
regInfo := allocation.Register(iter.Next())
regSource := regInfo.Sources[0]
//
if len(regInfo.Sources) != 1 {
// This should be unreachable.
panic("register not associated with unique column")
} else if regInfo.Sources[0].perspective != "" {
allocator.allocatePerspective(regInfo.Sources[0].perspective)
} else if regSource.IsVirtual() {
allocator.allocatePerspective(regSource.Perspective())
}
}
// Initial allocation of perspective registers
for iter := allocation.Registers(); iter.HasNext(); {
regIndex := iter.Next()
regInfo := allocation.Register(regIndex)

if regInfo.Sources[0].perspective != "" {
allocator.allocateRegister(regInfo.Sources[0].perspective, regIndex)
regSource := regInfo.Sources[0]
//
if regSource.IsVirtual() {
perspective := regSource.Perspective()
allocator.allocateRegister(perspective, regIndex)
}
}
// Done (for now)
Expand Down
42 changes: 20 additions & 22 deletions pkg/corset/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,9 @@ import (

"github.com/consensys/go-corset/pkg/sexp"
tr "github.com/consensys/go-corset/pkg/trace"
"github.com/consensys/go-corset/pkg/util"
)

// BindingId is an identifier is used to distinguish different forms of binding,
// as some forms are known from their use. Specifically, at the current time,
// only functions are distinguished from other categories (e.g. columns,
// parameters, etc).
type BindingId struct {
// Name of the binding
name string
// Indicates whether function binding or other.
fn bool
}

// Binding represents an association between a name, as found in a source file,
// and concrete item (e.g. a column, function, etc).
type Binding interface {
Expand Down Expand Up @@ -145,12 +135,14 @@ func (p *FunctionSignature) Apply(args []Expr, srcmap *sexp.SourceMaps[Node]) Ex

// ColumnBinding represents something bound to a given column.
type ColumnBinding struct {
// Column's enclosing module
module string
// Enclosing perspective
perspective string
// Column's name
name string
// Context determines the real (i.e. non-virtual) enclosing module of this
// column, and should always be a prefix of the path. If this column was
// declared in a perspective then it will be the perspective's enclosing
// module. Otherwise, it will exactly match the path's parent.
context util.Path
// Absolute path of column. This determines the name of the column, its
// enclosing module and/or perspective.
path util.Path
// Determines whether this is a computed column, or not.
computed bool
// Determines whether this column must be proven (or not).
Expand All @@ -167,8 +159,13 @@ type ColumnBinding struct {
// definterleaved constraint the target column information (e.g. its type) is
// not immediately available and must be determined from those columns from
// which it is constructed.
func NewComputedColumnBinding(module string, name string) *ColumnBinding {
return &ColumnBinding{module, "", name, true, false, 0, nil}
func NewComputedColumnBinding(context util.Path, path util.Path) *ColumnBinding {
return &ColumnBinding{context, path, true, false, 0, nil}
}

// AbsolutePath returns the fully resolved (absolute) path of the column in question.
func (p *ColumnBinding) AbsolutePath() *util.Path {
return &p.path
}

// IsFinalised checks whether this binding has been finalised yet or not.
Expand All @@ -185,7 +182,7 @@ func (p *ColumnBinding) Finalise(multiplier uint, datatype Type) {
// Context returns the of this column. That is, the module in which this colunm
// was declared and also the length multiplier of that module it requires.
func (p *ColumnBinding) Context() Context {
return tr.NewContext(p.module, p.multiplier)
return tr.NewContext(p.context.String(), p.multiplier)
}

// ============================================================================
Expand All @@ -194,6 +191,7 @@ func (p *ColumnBinding) Context() Context {

// ConstantBinding represents a constant definition
type ConstantBinding struct {
path util.Path
// Constant expression which, when evaluated, produces a constant value.
value Expr
// Inferred type of the given expression
Expand All @@ -202,8 +200,8 @@ type ConstantBinding struct {

// NewConstantBinding creates a new constant binding (which is initially not
// finalised).
func NewConstantBinding(value Expr) ConstantBinding {
return ConstantBinding{value, nil}
func NewConstantBinding(path util.Path, value Expr) ConstantBinding {
return ConstantBinding{path, value, nil}
}

// IsFinalised checks whether this binding has been finalised yet or not.
Expand Down
2 changes: 1 addition & 1 deletion pkg/corset/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ func (p *Compiler) Compile() (*hir.Schema, []SyntaxError) {
return nil, errs
}
// Convert global scope into an environment by allocating all columns.
environment := scope.ToEnvironment(p.allocator)
environment := NewGlobalEnvironment(scope, p.allocator)
// Finally, translate everything and add it to the schema.
return TranslateCircuit(environment, p.srcmap, &p.circuit)
}
Expand Down
67 changes: 48 additions & 19 deletions pkg/corset/declaration.go
Original file line number Diff line number Diff line change
Expand Up @@ -183,12 +183,12 @@ func (p *DefColumns) Lisp() sexp.SExp {
// DefColumn packages together those piece relevant to declaring an individual
// column, such its name and type.
type DefColumn struct {
// Column name
name string
// Binding of this column (which may or may not be finalised).
binding ColumnBinding
}

var _ SymbolDefinition = &DefColumn{}

// IsFunction is never true for a column definition.
func (e *DefColumn) IsFunction() bool {
return false
Expand All @@ -200,9 +200,16 @@ func (e *DefColumn) Binding() Binding {
return &e.binding
}

// Name of symbol being defined
// Name returns the (unqualified) name of this symbol. For example, "X" for
// a column X defined in a module m1.
func (e *DefColumn) Name() string {
return e.name
return e.binding.path.Tail()
}

// Path returns the qualified name (i.e. absolute path) of this symbol. For
// example, "m1.X" for a column X defined in module m1.
func (e *DefColumn) Path() *util.Path {
return &e.binding.path
}

// DataType returns the type of this column. If this column have not yet been
Expand Down Expand Up @@ -241,7 +248,7 @@ func (e *DefColumn) MustProve() bool {
// for debugging purposes.
func (e *DefColumn) Lisp() sexp.SExp {
list := sexp.EmptyList()
list.Append(sexp.NewSymbol(e.name))
list.Append(sexp.NewSymbol(e.Name()))
//
if e.binding.dataType != nil {
datatype := e.binding.dataType.String()
Expand Down Expand Up @@ -326,7 +333,7 @@ func (p *DefConst) Lisp() sexp.SExp {
def.Append(sexp.NewSymbol("defconst"))
//
for _, c := range p.constants {
def.Append(sexp.NewSymbol(c.name))
def.Append(sexp.NewSymbol(c.Name()))
def.Append(c.binding.value.Lisp())
}
// Done
Expand All @@ -336,8 +343,6 @@ func (p *DefConst) Lisp() sexp.SExp {
// DefConstUnit represents the definition of exactly one constant value. As
// such, this is an instance of SymbolDefinition and provides a binding.
type DefConstUnit struct {
// Name of the constant being declared.
name string
// Binding for this constant.
binding ConstantBinding
}
Expand All @@ -353,19 +358,26 @@ func (e *DefConstUnit) Binding() Binding {
return &e.binding
}

// Name of symbol being defined
// Name returns the (unqualified) name of this symbol. For example, "X" for
// a column X defined in a module m1.
func (e *DefConstUnit) Name() string {
return e.name
return e.binding.path.Tail()
}

// Path returns the qualified name (i.e. absolute path) of this symbol. For
// example, "m1.X" for a column X defined in module m1.
func (e *DefConstUnit) Path() *util.Path {
return &e.binding.path
}

// Lisp converts this node into its lisp representation. This is primarily used
// for debugging purposes.
//
//nolint:revive
func (p *DefConstUnit) Lisp() sexp.SExp {
func (e *DefConstUnit) Lisp() sexp.SExp {
return sexp.NewList([]sexp.SExp{
sexp.NewSymbol(p.name),
p.binding.value.Lisp()})
sexp.NewSymbol(e.Name()),
e.binding.value.Lisp()})
}

// ============================================================================
Expand Down Expand Up @@ -766,9 +778,16 @@ type DefPerspective struct {
Columns []*DefColumn
}

// Name of symbol being defined
// Name returns the (unqualified) name of this symbol. For example, "X" for
// a column X defined in a module m1.
func (p *DefPerspective) Name() string {
return p.symbol.name
return p.symbol.Path().Tail()
}

// Path returns the qualified name (i.e. absolute path) of this symbol. For
// example, "m1.X" for a column X defined in module m1.
func (p *DefPerspective) Path() *util.Path {
return &p.symbol.path
}

// IsFunction is always true for a function definition!
Expand Down Expand Up @@ -903,6 +922,8 @@ type DefFun struct {
parameters []*DefParameter
}

var _ SymbolDefinition = &DefFun{}

// IsFunction is always true for a function definition!
func (p *DefFun) IsFunction() bool {
return true
Expand Down Expand Up @@ -932,9 +953,16 @@ func (p *DefFun) Binding() Binding {
return p.symbol.binding
}

// Name of symbol being defined
// Name returns the (unqualified) name of this symbol. For example, "X" for
// a column X defined in a module m1.
func (p *DefFun) Name() string {
return p.symbol.name
return p.symbol.Path().Tail()
}

// Path returns the qualified name (i.e. absolute path) of this symbol. For
// example, "m1.X" for a column X defined in module m1.
func (p *DefFun) Path() *util.Path {
return &p.symbol.path
}

// Definitions returns the set of symbols defined by this declaration. Observe
Expand All @@ -951,7 +979,8 @@ func (p *DefFun) Dependencies() util.Iterator[Symbol] {
// Filter out all parameters declared in this function, since these are not
// external dependencies.
for _, d := range deps {
if d.IsQualified() || d.IsFunction() || !p.hasParameter(d.Name()) {
n := d.Path()
if n.IsAbsolute() || d.IsFunction() || n.Depth() > 1 || !p.hasParameter(n.Head()) {
ndeps = append(ndeps, d)
}
}
Expand All @@ -976,7 +1005,7 @@ func (p *DefFun) IsFinalised() bool {
func (p *DefFun) Lisp() sexp.SExp {
return sexp.NewList([]sexp.SExp{
sexp.NewSymbol("defun"),
sexp.NewSymbol(p.symbol.name),
sexp.NewSymbol(p.symbol.path.Tail()),
sexp.NewSymbol("..."), // todo
})
}
Expand Down
Loading

0 comments on commit 7ab511b

Please sign in to comment.