diff --git a/constraint/log.go b/constraint/log.go index c7d4445fc..d11a524fe 100644 --- a/constraint/log.go +++ b/constraint/log.go @@ -11,10 +11,11 @@ import ( // to represent string values (in logs or debug info) where a value is not known at compile time // (which is the case for variables that need to be resolved in the R1CS) type LogEntry struct { - Caller string - Format string - ToResolve []LinearExpression // TODO @gbotrel we could store here a struct with a flag that says if we expand or evaluate the expression - Stack []int + Caller string + Format string + ToResolve []LinearExpression // TODO @gbotrel we could store here a struct with a flag that says if we expand or evaluate the expression + Stack []int + FormatSpecifiers []string // Format specifiers for each value in ToResolve } func (l *LogEntry) WriteVariable(le LinearExpression, sbb *strings.Builder) { diff --git a/constraint/r1cs.go b/constraint/r1cs.go index 05b3fe8ae..5329b2250 100644 --- a/constraint/r1cs.go +++ b/constraint/r1cs.go @@ -17,6 +17,9 @@ type R1CS interface { // GetR1CIterator returns an R1CIterator to iterate on the R1C constraints of the system. GetR1CIterator() R1CIterator + + // GetLogs returns all log entries + GetLogs() []LogEntry } // R1CIterator facilitates iterating through R1C constraints. diff --git a/examples/printf/printf_example.go b/examples/printf/printf_example.go new file mode 100644 index 000000000..119864bfc --- /dev/null +++ b/examples/printf/printf_example.go @@ -0,0 +1,31 @@ +package examples + +import ( + "github.com/consensys/gnark/frontend" +) + +// PrintfCircuit demonstrates the usage of Printf for debugging circuit variables +type PrintfCircuit struct { + X, Y frontend.Variable `gnark:"x,public"` + Z frontend.Variable `gnark:"z,secret"` +} + +// Define implements the circuit logic using Printf for debugging +func (circuit *PrintfCircuit) Define(api frontend.API) error { + // Basic arithmetic with debug output + sum := api.Add(circuit.X, circuit.Y) + api.Printf("Sum of %d and %d is %d", circuit.X, circuit.Y, sum) + + // Show different number formats + api.Printf("X in different formats: dec=%d hex=%x bin=%b", circuit.X, circuit.X, circuit.X) + + // Debug intermediate calculations + product := api.Mul(sum, circuit.Z) + api.Printf("Product of sum(%d) and secret Z is %d", sum, product) + + // Verify constraints with debug output + isLessOrEqual := api.IsZero(api.Sub(api.Mul(circuit.X, circuit.Y), product)) + api.Printf("Is X*Y <= sum*Z? %d", isLessOrEqual) + + return nil +} diff --git a/examples/printf/printf_example_test.go b/examples/printf/printf_example_test.go new file mode 100644 index 000000000..8edb712e7 --- /dev/null +++ b/examples/printf/printf_example_test.go @@ -0,0 +1,18 @@ +package examples + +import ( + "testing" + + "github.com/consensys/gnark/test" +) + +func TestPrintfExample(t *testing.T) { + assert := test.NewAssert(t) + + var circuit PrintfCircuit + assert.ProverSucceeded(&circuit, &PrintfCircuit{ + X: 2, + Y: 3, + Z: 4, + }) +} diff --git a/frontend/api.go b/frontend/api.go index 40a6fdfab..a1fa50129 100644 --- a/frontend/api.go +++ b/frontend/api.go @@ -128,6 +128,15 @@ type API interface { // whose value will be resolved at runtime when computed by the solver Println(a ...Variable) + // Printf behaves like fmt.Printf but accepts cd.Variable as parameter + // whose value will be resolved at runtime when computed by the solver. + // Supported format specifiers: + // %v - default format + // %d - decimal integer + // %x - hexadecimal + // %b - binary + Printf(format string, a ...Variable) + // Compiler returns the compiler object for advanced circuit development Compiler() Compiler diff --git a/frontend/cs/r1cs/api.go b/frontend/cs/r1cs/api.go index c0763934a..3f3b49fd8 100644 --- a/frontend/cs/r1cs/api.go +++ b/frontend/cs/r1cs/api.go @@ -824,3 +824,57 @@ func (builder *builder) wireIDsToVars(wireIDs ...[]int) []frontend.Variable { func (builder *builder) SetGkrInfo(info constraint.GkrInfo) error { return builder.cs.AddGkr(info) } + +// Printf enables circuit debugging and behaves like fmt.Printf() +// Format specifiers: +// %v - default format +// %d - decimal integer +// %x - hexadecimal +// %b - binary +func (builder *builder) Printf(format string, a ...frontend.Variable) { + var log constraint.LogEntry + + // prefix log line with file.go:line + if _, file, line, ok := runtime.Caller(1); ok { + log.Caller = fmt.Sprintf("%s:%d", filepath.Base(file), line) + } + + var sbb strings.Builder + argIndex := 0 + + for i := 0; i < len(format); { + if i+1 < len(format) && format[i] == '%' { + if argIndex >= len(a) { + panic("Printf: not enough arguments for format string") + } + + switch format[i+1] { + case 'v', 'd', 'x', 'b': + if v, ok := a[argIndex].(expr.LinearExpression); ok { + assertIsSet(v) + sbb.WriteString("%s") + // Store format specifier with the value + log.ToResolve = append(log.ToResolve, builder.getLinearExpression(v)) + log.FormatSpecifiers = append(log.FormatSpecifiers, string(format[i+1])) + } else { + builder.printArg(&log, &sbb, a[argIndex]) + } + argIndex++ + i += 2 + case '%': + sbb.WriteByte('%') + i += 2 + default: + sbb.WriteByte(format[i]) + i++ + } + } else { + sbb.WriteByte(format[i]) + i++ + } + } + + // set format string to be used with fmt.Sprintf + log.Format = sbb.String() + builder.cs.AddLog(log) +} diff --git a/frontend/cs/r1cs/api_test.go b/frontend/cs/r1cs/api_test.go new file mode 100644 index 000000000..f3db803fa --- /dev/null +++ b/frontend/cs/r1cs/api_test.go @@ -0,0 +1,53 @@ +package r1cs + +import ( + "testing" + + "github.com/consensys/gnark/frontend" + "github.com/consensys/gnark/frontend/schema" + "github.com/consensys/gnark/internal/tinyfield" +) + +func TestPrintf(t *testing.T) { + var circuit struct { + X, Y frontend.Variable + } + + builder := newBuilder(tinyfield.Modulus(), frontend.CompileConfig{}) + + // Create LeafInfo for variables + xInfo := schema.LeafInfo{ + FullName: func() string { return "X" }, + Visibility: schema.Public, + } + yInfo := schema.LeafInfo{ + FullName: func() string { return "Y" }, + Visibility: schema.Public, + } + + circuit.X = builder.PublicVariable(xInfo) + circuit.Y = builder.PublicVariable(yInfo) + + // Test different format specifiers + builder.Printf("X in different formats: dec=%d hex=%x bin=%b default=%v", circuit.X, circuit.X, circuit.X, circuit.X) + + // Test multiple variables + builder.Printf("X=%d Y=%d sum=%d", circuit.X, circuit.Y, builder.Add(circuit.X, circuit.Y)) + + // Test escaping %% + builder.Printf("100%% sure that X=%d", circuit.X) + + // Get logs from constraint system + logs := builder.cs.GetLogs() + if len(logs) != 3 { + t.Errorf("expected 3 log entries, got %d", len(logs)) + } + + // Verify format specifiers are stored correctly + if len(logs[0].FormatSpecifiers) != 4 { + t.Errorf("expected 4 format specifiers, got %d", len(logs[0].FormatSpecifiers)) + } + if logs[0].FormatSpecifiers[0] != "d" { + t.Errorf("expected 'd' format specifier, got %s", logs[0].FormatSpecifiers[0]) + } +} diff --git a/frontend/cs/scs/api.go b/frontend/cs/scs/api.go index ae919d67b..cd162b48c 100644 --- a/frontend/cs/scs/api.go +++ b/frontend/cs/scs/api.go @@ -684,3 +684,56 @@ func (*builder) FrontendType() frontendtype.Type { func (builder *builder) SetGkrInfo(info constraint.GkrInfo) error { return builder.cs.AddGkr(info) } + +// Printf enables circuit debugging and behaves like fmt.Printf() +// Format specifiers: +// %v - default format +// %d - decimal integer +// %x - hexadecimal +// %b - binary +func (builder *builder) Printf(format string, a ...frontend.Variable) { + var log constraint.LogEntry + + // prefix log line with file.go:line + if _, file, line, ok := runtime.Caller(1); ok { + log.Caller = fmt.Sprintf("%s:%d", filepath.Base(file), line) + } + + var sbb strings.Builder + argIndex := 0 + + for i := 0; i < len(format); { + if i+1 < len(format) && format[i] == '%' { + if argIndex >= len(a) { + panic("Printf: not enough arguments for format string") + } + + switch format[i+1] { + case 'v', 'd', 'x', 'b': + if v, ok := a[argIndex].(expr.Term); ok { + sbb.WriteString("%s") + // Store format specifier with the value + log.ToResolve = append(log.ToResolve, constraint.LinearExpression{builder.cs.MakeTerm(v.Coeff, v.VID)}) + log.FormatSpecifiers = append(log.FormatSpecifiers, string(format[i+1])) + } else { + builder.printArg(&log, &sbb, a[argIndex]) + } + argIndex++ + i += 2 + case '%': + sbb.WriteByte('%') + i += 2 + default: + sbb.WriteByte(format[i]) + i++ + } + } else { + sbb.WriteByte(format[i]) + i++ + } + } + + // set format string to be used with fmt.Sprintf + log.Format = sbb.String() + builder.cs.AddLog(log) +} diff --git a/frontend/cs/scs/api_test.go b/frontend/cs/scs/api_test.go index 10ed705ca..6574f7bf3 100644 --- a/frontend/cs/scs/api_test.go +++ b/frontend/cs/scs/api_test.go @@ -6,6 +6,8 @@ import ( "github.com/consensys/gnark-crypto/ecc" "github.com/consensys/gnark/frontend" "github.com/consensys/gnark/frontend/cs/scs" + "github.com/consensys/gnark/frontend/schema" + "github.com/consensys/gnark/internal/tinyfield" "github.com/consensys/gnark/test" "github.com/stretchr/testify/require" ) @@ -290,3 +292,47 @@ func TestRegressionOr(t *testing.T) { } } } + +func TestPrintf(t *testing.T) { + var circuit struct { + X, Y frontend.Variable + } + + builder := newBuilder(tinyfield.Modulus(), frontend.CompileConfig{}) + + // Create LeafInfo for variables + xInfo := schema.LeafInfo{ + FullName: func() string { return "X" }, + Visibility: schema.Public, + } + yInfo := schema.LeafInfo{ + FullName: func() string { return "Y" }, + Visibility: schema.Public, + } + + circuit.X = builder.PublicVariable(xInfo) + circuit.Y = builder.PublicVariable(yInfo) + + // Test different format specifiers + builder.Printf("X in different formats: dec=%d hex=%x bin=%b default=%v", circuit.X, circuit.X, circuit.X, circuit.X) + + // Test multiple variables + builder.Printf("X=%d Y=%d sum=%d", circuit.X, circuit.Y, builder.Add(circuit.X, circuit.Y)) + + // Test escaping %% + builder.Printf("100%% sure that X=%d", circuit.X) + + // Get logs from constraint system + logs := builder.cs.GetLogs() + if len(logs) != 3 { + t.Errorf("expected 3 log entries, got %d", len(logs)) + } + + // Verify format specifiers are stored correctly + if len(logs[0].FormatSpecifiers) != 4 { + t.Errorf("expected 4 format specifiers, got %d", len(logs[0].FormatSpecifiers)) + } + if logs[0].FormatSpecifiers[0] != "d" { + t.Errorf("expected 'd' format specifier, got %s", logs[0].FormatSpecifiers[0]) + } +}