diff --git a/cmd/main.go b/cmd/main.go index e531aba..0d42340 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -150,7 +150,7 @@ func runCodeGen(darFile, outputDir, pkgFile string, debugMode bool, generateHexC dalfToProcess = append(dalfToProcess, manifest.MainDalf) dalfToProcess = append(dalfToProcess, dalfs...) - result, err := codegen.CodegenDalfs(dalfToProcess, reader, pkgFile, dalfManifest, generateHexCodec, model2.ExternalPackages{}) + result, err := codegen.CodegenDalfs(dalfToProcess, reader, pkgFile, dalfManifest, generateHexCodec, model2.ExternalPackages{}, model2.FieldHints{}) if err != nil { return err } diff --git a/codegen/astgen/factory.go b/codegen/astgen/factory.go index 4eefcb2..239c048 100644 --- a/codegen/astgen/factory.go +++ b/codegen/astgen/factory.go @@ -3,7 +3,7 @@ package astgen import ( "fmt" - "github.com/smartcontractkit/go-daml/codegen/astgen/v3" + v3 "github.com/smartcontractkit/go-daml/codegen/astgen/v3" model2 "github.com/smartcontractkit/go-daml/codegen/model" ) @@ -18,10 +18,10 @@ type AstGen interface { GetTemplateStructs(ifcByModule map[string]model2.InterfaceMap) (map[string]*model2.TmplStruct, model2.ExternalPackages, error) } -func GetAstGenFromVersion(payload []byte, ext model2.ExternalPackages, ver string) (AstGen, error) { +func GetAstGenFromVersion(payload []byte, ext model2.ExternalPackages, hints model2.FieldHints, ver string) (AstGen, error) { switch ver { case V3: - return v3.NewCodegenAst(payload, ext), nil + return v3.NewCodegenAst(payload, ext, hints), nil default: return nil, fmt.Errorf("none supported version") } diff --git a/codegen/astgen/v3/common.go b/codegen/astgen/v3/common.go index 64e6114..7d07573 100644 --- a/codegen/astgen/v3/common.go +++ b/codegen/astgen/v3/common.go @@ -30,12 +30,15 @@ type codeGenAst struct { // For keeping track of which packages have been imported, will start off empty and be populated as we process the // DAML LF and encounter references to external packages. This allows us to include only the necessary imports in the generated code importedPackages map[string]model.ExternalPackage + // Caller-supplied hints for fields that need non-default hex encoding tags + fieldHints model.FieldHints } -func NewCodegenAst(payload []byte, externalPackages model.ExternalPackages) *codeGenAst { +func NewCodegenAst(payload []byte, externalPackages model.ExternalPackages, fieldHints model.FieldHints) *codeGenAst { return &codeGenAst{ payload: payload, externalPackages: externalPackages, + fieldHints: fieldHints, } } @@ -216,10 +219,10 @@ func (c *codeGenAst) getTemplates( RawType: field.String(), IsOptional: isOptional, IsEnum: c.isEnumType(typeExtracted, pkg), - IsBytes: model.BytesFieldNames[fieldExtracted], - IsBytesHex: model.BytesHexFieldNames[fieldExtracted], - IsUint32: model.Uint32FieldNames[fieldExtracted], - IsUint32List: model.Uint32ListFieldNames[fieldExtracted], + IsBytes: c.fieldHints.BytesFields[fieldExtracted], + IsBytesHex: c.fieldHints.BytesHexFields[fieldExtracted], + IsUint32: c.fieldHints.Uint32Fields[fieldExtracted], + IsUint32List: c.fieldHints.Uint32ListFields[fieldExtracted], }) } default: @@ -439,10 +442,10 @@ func (c *codeGenAst) getDataTypes(pkg *daml.Package, module *daml.Module, module Type: typeExtracted, RawType: field.String(), IsOptional: isOptional, - IsBytes: model.BytesFieldNames[fieldExtracted], - IsBytesHex: model.BytesHexFieldNames[fieldExtracted], - IsUint32: model.Uint32FieldNames[fieldExtracted], - IsUint32List: model.Uint32ListFieldNames[fieldExtracted], + IsBytes: c.fieldHints.BytesFields[fieldExtracted], + IsBytesHex: c.fieldHints.BytesHexFields[fieldExtracted], + IsUint32: c.fieldHints.Uint32Fields[fieldExtracted], + IsUint32List: c.fieldHints.Uint32ListFields[fieldExtracted], }) } case *daml.DefDataType_Variant: @@ -457,10 +460,10 @@ func (c *codeGenAst) getDataTypes(pkg *daml.Package, module *daml.Module, module Type: typeExtracted, RawType: field.String(), IsOptional: true, - IsBytes: model.BytesFieldNames[fieldExtracted], - IsBytesHex: model.BytesHexFieldNames[fieldExtracted], - IsUint32: model.Uint32FieldNames[fieldExtracted], - IsUint32List: model.Uint32ListFieldNames[fieldExtracted], + IsBytes: c.fieldHints.BytesFields[fieldExtracted], + IsBytesHex: c.fieldHints.BytesHexFields[fieldExtracted], + IsUint32: c.fieldHints.Uint32Fields[fieldExtracted], + IsUint32List: c.fieldHints.Uint32ListFields[fieldExtracted], }) } case *daml.DefDataType_Enum: diff --git a/codegen/codegen.go b/codegen/codegen.go index e66341f..fcefeab 100644 --- a/codegen/codegen.go +++ b/codegen/codegen.go @@ -62,7 +62,7 @@ func GetManifest(dar fs.FS) (*model2.Manifest, error) { return manifest, nil } -func CodegenDalfs(dalfToProcess []string, dar fs.FS, pkgFile string, dalfManifest *model2.Manifest, generateHexCodec bool, externalPackages model2.ExternalPackages) (map[string]string, error) { +func CodegenDalfs(dalfToProcess []string, dar fs.FS, pkgFile string, dalfManifest *model2.Manifest, generateHexCodec bool, externalPackages model2.ExternalPackages, fieldHints model2.FieldHints) (map[string]string, error) { // ensure stable processing order across runs sort.Strings(dalfToProcess) @@ -137,7 +137,7 @@ func CodegenDalfs(dalfToProcess []string, dar fs.FS, pkgFile string, dalfManifes continue } - pkg, err := GetAST(dalfContent, dalfManifest, ifcByModule, externalPackages) + pkg, err := GetAST(dalfContent, dalfManifest, ifcByModule, externalPackages, fieldHints) if err != nil { return nil, fmt.Errorf("failed to generate AST: %w", err) } @@ -268,7 +268,7 @@ func GetInterfaces(payload []byte, manifest *model2.Manifest) (map[string]*model return nil, fmt.Errorf("unsupported sdk version %s", manifest.SdkVersion) } - gen, err := astgen.GetAstGenFromVersion(payload, model2.ExternalPackages{}, version) + gen, err := astgen.GetAstGenFromVersion(payload, model2.ExternalPackages{}, model2.FieldHints{}, version) if err != nil { return nil, err } @@ -276,7 +276,7 @@ func GetInterfaces(payload []byte, manifest *model2.Manifest) (map[string]*model return gen.GetInterfaces() } -func GetAST(payload []byte, manifest *model2.Manifest, ifcByModule map[string]model2.InterfaceMap, externalPackages model2.ExternalPackages) (*model2.Package, error) { +func GetAST(payload []byte, manifest *model2.Manifest, ifcByModule map[string]model2.InterfaceMap, externalPackages model2.ExternalPackages, fieldHints model2.FieldHints) (*model2.Package, error) { var version string if strings.HasPrefix(manifest.SdkVersion, astgen.V3) { version = astgen.V3 @@ -286,7 +286,7 @@ func GetAST(payload []byte, manifest *model2.Manifest, ifcByModule map[string]mo return nil, fmt.Errorf("unsupported sdk version %s", manifest.SdkVersion) } - gen, err := astgen.GetAstGenFromVersion(payload, externalPackages, version) + gen, err := astgen.GetAstGenFromVersion(payload, externalPackages, fieldHints, version) if err != nil { return nil, err } diff --git a/codegen/codegen_test.go b/codegen/codegen_test.go index c1b9994..906a476 100644 --- a/codegen/codegen_test.go +++ b/codegen/codegen_test.go @@ -34,7 +34,7 @@ func TestGetMainDalfV3(t *testing.T) { require.NoError(t, err) require.NotNil(t, dalfContent) - ast, err := GetAST(dalfContent, manifest, nil, model.ExternalPackages{}) + ast, err := GetAST(dalfContent, manifest, nil, model.ExternalPackages{}, model.FieldHints{}) require.Nil(t, err) require.NotEmpty(t, ast.Structs) diff --git a/codegen/model/types.go b/codegen/model/types.go index 7f3f007..3f1adaa 100644 --- a/codegen/model/types.go +++ b/codegen/model/types.go @@ -80,65 +80,22 @@ func (t BytesHex) IsBytesHex() bool { // The hardcoded maps (BytesFieldNames, BytesHexFieldNames) work around this limitation by explicitly listing // field names that need hex encoding tags. -// BytesHexFieldNames contains field names that should use uint16 length prefix -// encoding (hex:"bytes16" tag) instead of the default uint8 length prefix. +// FieldHints allows callers to declare which struct field names need non-default +// hex encoding tags. Because the Daml compiler erases type synonyms (e.g. BytesHex +// becomes Text in the compiled Daml-LF), go-daml cannot infer the correct encoding +// from the .dalf alone. Callers that know the encoding semantics of their contracts +// populate these maps and pass a FieldHints value to CodegenDalfs / GetAST. // -// Background: The Daml BytesHex type synonym is expanded to Text by the compiler, -// so we cannot detect it from the compiled Daml LF. Instead, we use a field name -// allowlist for fields that are known to potentially exceed 255 bytes. -// -// Fields requiring bytes16 encoding (from MCMS/Codec.daml): -// - operationData: Serialized choice parameters in TimelockCall (encodeUint16) -// - predecessor: Hex hash in ScheduleBatchParams (encodeUint16) -// - salt: Hex value in ScheduleBatchParams (encodeUint16) -// -// To add a new field: add its name (case-sensitive) as a key with value true. -var BytesHexFieldNames = map[string]bool{ - "operationData": true, - "predecessor": true, - "salt": true, -} - -// BytesFieldNames contains field names that should use uint8 length prefix -// encoding (hex:"bytes" tag). These are fixed-size hex fields ≤255 bytes. -// -// From MCMS/CCIP contracts: -// - signerAddress: EVM signer address (20 bytes) -// - chainFamilySelector: Chain family selector (4 bytes) -// - root: Merkle root hash (32 bytes) -// - newRoot: New merkle root hash (32 bytes) -var BytesFieldNames = map[string]bool{ - "signerAddress": true, - "chainFamilySelector": true, - "root": true, - "newRoot": true, -} - -// Uint32FieldNames contains field names where INT64 should encode as 4-byte uint32. -// This matches the Daml MCMS/Codec.daml encoding which uses encodeUint32 for these fields. -// -// From MCMS/Codec.daml encodeSignerInfo: -// - signerIndex: encodeUint32 (4 bytes) -// - signerGroup: encodeUint32 (4 bytes) -var Uint32FieldNames = map[string]bool{ - "signerIndex": true, - "signerGroup": true, -} - -// Uint32ListFieldNames contains field names where []INT64 should encode as -// length + uint32 elements (4 bytes each) instead of uint64 elements (8 bytes). -// This matches the Daml MCMS/Codec.daml encoding which uses encodeUint32 for each element. -// -// From MCMS/Codec.daml encodeSetConfigParams and encodeMultisigConfig: -// - groupQuorums: list of encodeUint32 (4 bytes each) -// - groupParents: list of encodeUint32 (4 bytes each) -// - apGroupQuorums: used in AdminParams.AP_SetConfig -// - apGroupParents: used in AdminParams.AP_SetConfig -var Uint32ListFieldNames = map[string]bool{ - "groupQuorums": true, - "groupParents": true, - "apGroupQuorums": true, - "apGroupParents": true, +// An empty (zero-value) FieldHints is valid and means no special encoding is applied. +type FieldHints struct { + // BytesFields: field names that should receive a hex:"bytes" tag (uint8 length prefix, ≤255 bytes). + BytesFields map[string]bool + // BytesHexFields: field names that should receive a hex:"bytes16" tag (uint16 length prefix). + BytesHexFields map[string]bool + // Uint32Fields: field names where INT64 should be encoded as a 4-byte uint32 (hex:"uint32" tag). + Uint32Fields map[string]bool + // Uint32ListFields: field names where []INT64 should be encoded as []uint32 (hex:"[]uint32" tag). + Uint32ListFields map[string]bool } type Int64 struct { diff --git a/codegen/template_test.go b/codegen/template_test.go index b4333ae..78d2dbb 100644 --- a/codegen/template_test.go +++ b/codegen/template_test.go @@ -109,15 +109,29 @@ func TestDecapitalize(t *testing.T) { } } -func TestBytesHexFieldNames(t *testing.T) { - // Test that BytesHexFieldNames contains expected entries - if !model.BytesHexFieldNames["operationData"] { - t.Error("BytesHexFieldNames should contain 'operationData'") +func TestFieldHints(t *testing.T) { + // An empty FieldHints should not match any field name + empty := model.FieldHints{} + if empty.BytesHexFields["operationData"] { + t.Error("empty FieldHints.BytesHexFields should not match any field") + } + if empty.BytesFields["signerAddress"] { + t.Error("empty FieldHints.BytesFields should not match any field") } - // Test that non-BytesHex fields are not in the map - if model.BytesHexFieldNames["someOtherField"] { - t.Error("BytesHexFieldNames should not contain 'someOtherField'") + // A populated FieldHints should match exactly the configured fields + hints := model.FieldHints{ + BytesHexFields: map[string]bool{"operationData": true}, + BytesFields: map[string]bool{"signerAddress": true}, + } + if !hints.BytesHexFields["operationData"] { + t.Error("hints.BytesHexFields should contain 'operationData'") + } + if hints.BytesHexFields["someOtherField"] { + t.Error("hints.BytesHexFields should not contain 'someOtherField'") + } + if !hints.BytesFields["signerAddress"] { + t.Error("hints.BytesFields should contain 'signerAddress'") } }