From 7781cf26b534cc22e9257724664f644d790cd572 Mon Sep 17 00:00:00 2001 From: Elyan Date: Tue, 23 Dec 2025 17:27:44 +0100 Subject: [PATCH] Initial commit --- Makefile | 21 ++++ README.md | 9 ++ go.mod | 11 ++ go.sum | 10 ++ query/indexer.go | 116 ++++++++++++++++++ query/ref.go | 229 ++++++++++++++++++++++++++++++++++ query/ref_test.go | 31 +++++ query/tree.go | 61 +++++++++ spec/models.go | 307 ++++++++++++++++++++++++++++++++++++++++++++++ spec/visitor.go | 5 + 10 files changed, 800 insertions(+) create mode 100644 Makefile create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 query/indexer.go create mode 100644 query/ref.go create mode 100644 query/ref_test.go create mode 100644 query/tree.go create mode 100644 spec/models.go create mode 100644 spec/visitor.go diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a1b2374 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +GO_MODULE_IMPORT := go.datafortress.dev/pkg/central-arch-spec-models +GO_MODELS_PKG := spec + +SPEC_XML_NS := https://datafortress.dev/xml/central-arch-specification +SPEC_SCHEMA_DIR := ../central-arch-spec-schema +SPEC_SCHEMA_FILE := specification.xsd + +.PHONY: all +all: tests + +.PHONY: tests +tests: + go test $(shell find . -type f -name '*_test.go' -print) + +.PHONY: deps +deps: + go install github.com/gocomply/xsd2go/cli/gocomply_xsd2go@latest + +.PHONY: generate +generate: + gocomply_xsd2go convert --xmlns-override=${SPEC_XML_NS}=${GO_MODELS_PKG} ${SPEC_SCHEMA_DIR}/${SPEC_SCHEMA_FILE} ${GO_MODULE_IMPORT} . diff --git a/README.md b/README.md new file mode 100644 index 0000000..c34beb3 --- /dev/null +++ b/README.md @@ -0,0 +1,9 @@ +# Central Arch: XML Specification Models +## Install GoComply Xsd2Go +```bash +make deps +``` +## Regenerate data model from XSD +```bash +make models +``` diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6eb76fa --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module go.datafortress.dev/pkg/central-arch-spec-models + +go 1.23.2 + +require github.com/stretchr/testify v1.11.1 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c4c1710 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/query/indexer.go b/query/indexer.go new file mode 100644 index 0000000..d37cfd7 --- /dev/null +++ b/query/indexer.go @@ -0,0 +1,116 @@ +package query + +import "fmt" + +type IndexedElement struct { + Name string + DisplayName *string + ReferencedData any + Parent *IndexedContainer + Containers []*IndexedContainer +} + +func (e *IndexedElement) Init() { + for _, container := range e.Containers { + container.Parent = e + container.Init() + } +} + +func (e *IndexedElement) String() string { + if e.Parent == nil { + panic("IndexedElement has no parent!") + } + return fmt.Sprintf("%s:%s", e.Parent.Parent, e.Name) +} + +func (e *IndexedElement) PrintableName() string { + var result = e.Name + if e.DisplayName != nil { + result = *e.DisplayName + } + return result +} + +func (e *IndexedElement) Find(c *Container) QueryResult { + return findInContainers(c, e.Containers) +} + +type IndexedContainer struct { + Name string + Parent *IndexedElement + Elements []*IndexedElement +} + +func (c *IndexedContainer) Init() { + for _, element := range c.Elements { + element.Parent = c + element.Init() + } +} + +func (c *IndexedContainer) String() string { + var parentStr = "" + + if c.Parent != nil { + parentStr = c.Parent.String() + } + return fmt.Sprintf("%s%s", parentStr, c.Name) +} + +func (c *IndexedContainer) PrintableName() string { + return c.Name +} + +func (c *IndexedContainer) Find(e *Element) QueryResult { + for _, element := range c.Elements { + if e.Name == element.Name { + if e.NextContainer != nil { + // If the element is found but the query expression continues + // with a container query, delegate the search to the element + return element.Find(e.NextContainer) + } else { + // If not, return the found element + return QueryResult{element} + } + } + } + // Return an empty list if the element is not found + return make(QueryResult, 0) +} + +// A query tree is a bipartite graph composed of containers and elements. +// Only elements can be queried. +type IndexedRoot struct { + Containers []*IndexedContainer +} + +func (r *IndexedRoot) Find(q *Query) QueryResult { + return findInContainers(&q.Container, r.Containers) +} + +func (r *IndexedRoot) FindByRef(ref Reference) (QueryResult, error) { + var query, err = ref.Parse() + + if err != nil { + return nil, err + } + return r.Find(query), nil +} + +func findInContainers(queried *Container, containers []*IndexedContainer) QueryResult { + for _, container := range containers { + if queried.Name == container.Name { + if queried.NextElement != nil { + // If the container is found but the query expression continues + // with an element query, delegate the search to the container + return container.Find(queried.NextElement) + } else { + // If not, return all the elements in the found container + return container.Elements + } + } + } + // Return an empty list if the container is not found + return make(QueryResult, 0) +} diff --git a/query/ref.go b/query/ref.go new file mode 100644 index 0000000..ac847cc --- /dev/null +++ b/query/ref.go @@ -0,0 +1,229 @@ +package query + +import ( + "fmt" + "strings" + "unicode" + "unicode/utf8" +) + +/* +A reference is composed of one or more matchers separated by periods. +A matcher can be composed by a single container name or by both a +container name and an element identifier. +A matcher only composed of a container name can only appear as the last +matcher of a reference. + +A reference the last matcher of which is composed of both a container +name and an element identifier is a fully qualified reference. + +A reference the last matcher of which is only composed of a container +returns all elements inside the container or an empty list. +A reference the last matcher of which is composed of both a container +name and an element identifier returns only the referenced element or +an empty list. + +Example, given the following query tree: + + (with "r:" meaning "root", "c:" container and "e:" element). + + -+---+-- + | |-- + | + +----+---+-- + +Ref: "format" +Returns: [T1, T2] + +Ref: "format:T1" +Returns: [T1] + +Ref: "instr:BKPT" +Returns: [BKPT] + +Ref: "instr:BKPT.binding" +Returns: [] + +Ref: "instr:BKPT.binding:base" +Returns: [] + +Ref: "instr.binding" +Returns: Error +=> Container-only matcher can only be used as the last matcher in a reference. +*/ +type Reference string + +func (ref *Reference) Parse() (*Query, error) { + var container, err = parseContainerComponent(*ref, 0) + + if err != nil { + return nil, err + } + return &Query{Container: *container}, nil +} + +func (ref *Reference) IsValid() bool { + var _, err = ref.Parse() + return err == nil +} + +func (ref *Reference) IsFullyQualified() bool { + var query, err = ref.Parse() + + if err != nil { + return false + } + + return query.IsFullyQualified() +} + +type RefParseError struct { + Ref Reference + Index int + Msg string +} + +func (err *RefParseError) Error() string { + var error_fmt = `Parsing error at character %d in the following query reference: +"%s" +%s^ +Error: %s` + + return fmt.Sprintf(error_fmt, err.Index+1, err.Ref, strings.Repeat(" ", err.Index), err.Msg) +} + +func isValidNameChar(r rune) bool { + return unicode.IsLetter(r) || unicode.IsDigit(r) || r == '_' || r == '-' +} + +func isMatcherSeparator(r rune) bool { + return r == '.' +} + +func isElementIdSeparator(r rune) bool { + return r == ':' +} + +func parseContainerComponent(ref Reference, index int) (*Container, error) { + var currentName = "" + + if index >= len(ref) { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: "Expected a container name but reached end of reference", + } + } + + for i, w := index, 0; i < len(ref); i += w { + var runeValue, width = utf8.DecodeRuneInString(string(ref[i:])) + w = width + + if isElementIdSeparator(runeValue) { + if len(currentName) > 0 { + var e, err = parseElementComponent(ref, index) + + if err != nil { + return nil, err + } + return &Container{Name: currentName, NextElement: e}, nil + } else { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: "Expected a container name before an element identifier", + } + } + } + + if isMatcherSeparator(runeValue) { + if len(currentName) > 0 { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: "Expected an element identifier before another container name", + } + } else { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: "Expected both a container name and an element identifier before another container name", + } + } + } + + if isValidNameChar(runeValue) { + currentName += string(runeValue) + } else { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: fmt.Sprintf("Character '%c' is not permitted in a container name", runeValue), + } + } + } + + return &Container{Name: currentName, NextElement: nil}, nil +} + +func parseElementComponent(ref Reference, index int) (*Element, error) { + var currentName = "" + + if index >= len(ref) { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: "Expected an element identifier but reached end of reference", + } + } + + for i, w := index, 0; i < len(ref); i += w { + var runeValue, width = utf8.DecodeRuneInString(string(ref[i:])) + w = width + + if isElementIdSeparator(runeValue) { + if len(currentName) > 0 { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: "Expected a container name before another element identifier", + } + } else { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: "Expected both a container name and an element identifier before another element identifier", + } + } + } + + if isMatcherSeparator(runeValue) { + if len(currentName) > 0 { + var e, err = parseContainerComponent(ref, index) + + if err != nil { + return nil, err + } + return &Element{Name: currentName, NextContainer: e}, nil + } else { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: "Expected an element identifier before a container name", + } + } + } + + if isValidNameChar(runeValue) { + currentName += string(runeValue) + } else { + return nil, &RefParseError{ + Ref: ref, + Index: index, + Msg: fmt.Sprintf("Character '%c' is not permitted in an element identifier", runeValue), + } + } + } + + return &Element{Name: currentName, NextContainer: nil}, nil +} diff --git a/query/ref_test.go b/query/ref_test.go new file mode 100644 index 0000000..0133ece --- /dev/null +++ b/query/ref_test.go @@ -0,0 +1,31 @@ +package query_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "go.datafortress.dev/pkg/central-arch-spec-models/query" +) + +const ( + MULTIPLE_LEVELS_QUERY_REF = "instruction:LDR_Base_Offset.binding:base" + FULLY_QUALIFIED_QUERY_REF = "c1:e1.c2:e2" + NOT_FULLY_QUALIFIED_QUERY_REF = "c1:e1.c2" +) + +func TestRefParsing(t *testing.T) { + var ref query.Reference = MULTIPLE_LEVELS_QUERY_REF + var query, err = ref.Parse() + + require.Nil(t, err) + + var instrContainer = query.Container + require.Equal(t, "instruction", instrContainer.Name) + require.NotNil(t, instrContainer.NextElement) + + var instrElement = instrContainer.NextElement + require.Equal(t, "LDR_Base_Offset", instrElement.Name) + require.NotNil(t, instrElement.NextContainer) + + // TODO Continue testing +} diff --git a/query/tree.go b/query/tree.go new file mode 100644 index 0000000..cc3164e --- /dev/null +++ b/query/tree.go @@ -0,0 +1,61 @@ +package query + +import ( + "fmt" +) + +type QueryResult []*IndexedElement + +type Element struct { + Name string + NextContainer *Container +} + +func (e *Element) String() string { + var containerStr = "" + + if e.NextContainer != nil { + containerStr = fmt.Sprintf(".%s", e.NextContainer) + } + return fmt.Sprintf("%s%s", e.Name, containerStr) +} + +type Container struct { + Name string + NextElement *Element +} + +func (c *Container) String() string { + var elementStr = "" + + if c.NextElement != nil { + elementStr = fmt.Sprintf(":%s", c.NextElement) + } + return fmt.Sprintf("%s%s", c.Name, elementStr) +} + +type Query struct { + Container Container +} + +func (q *Query) String() string { + return q.Container.String() +} + +func (q *Query) IsFullyQualified() bool { + var container *Container = &q.Container + + for container != nil { + if container.NextElement == nil { + // Current container has no next element, so the query ended on a container. + // Thus, it is not fully qualified. + return false + } else { + container = container.NextElement.NextContainer + } + } + + // Next container is nil, so the query ended on an element. + // Thus, it is fully qualified. + return true +} diff --git a/spec/models.go b/spec/models.go new file mode 100644 index 0000000..bb7a693 --- /dev/null +++ b/spec/models.go @@ -0,0 +1,307 @@ +// Code generated by https://github.com/gocomply/xsd2go; DO NOT EDIT. +// Models for https://datafortress.dev/xml/central-arch-specification +package spec + +import ( + "encoding/xml" +) + +// Element +type Specification struct { + XMLName xml.Name `xml:"specification"` + + Exceptions ExceptionList `xml:"exceptions"` + + Formats FormatList `xml:"formats"` + + Instructions InstructionList `xml:"instructions"` +} + +// XSD ComplexType declarations + +// SpecificationType: Central Architecture CPU XML Specification file. +type SpecificationType struct { + XMLName xml.Name + + Exceptions ExceptionList `xml:"exceptions"` + + Formats FormatList `xml:"formats"` + + Instructions InstructionList `xml:"instructions"` +} + +// FormatList: Supported instruction encoding formats. +type FormatList struct { + XMLName xml.Name + + Format []Format `xml:",any"` +} + +// InstructionList: Supported instructions. +type InstructionList struct { + XMLName xml.Name + + Instruction []Instruction `xml:",any"` +} + +// ExceptionList: Supported exceptions. +type ExceptionList struct { + XMLName xml.Name + + Exception []Exception `xml:",any"` +} + +// DocText: Documentation text. +type DocText struct { + XMLName xml.Name + + Ref []RefText `xml:"ref"` + + Todo []TodoText `xml:"todo"` + + InnerXml string `xml:",innerxml"` +} + +// RefText: A reference to an element. +type RefText struct { + XMLName xml.Name + + // Type: Reference type. Used to set a specific styling to the reference text. Can also generate a link to a reference in the documentation. + Type string `xml:"type,attr"` + + Text string `xml:",chardata"` +} + +// TodoText: A TODO annotation. +type TodoText struct { + XMLName xml.Name + + Text string `xml:",chardata"` +} + +// Exception: The specification of an exception. +type Exception struct { + XMLName xml.Name + + // Id: The unique identifier of this exception. + Id string `xml:"id,attr"` + + // DisplayName: The name of the exception that will be displayed in the documentation. + DisplayName string `xml:"display-name"` + + // Brief: A brief description of this exception. + Brief DocText `xml:"brief"` + + // Details: Details about this exception. Used to give info about where the exception handler PSW is stored and what are the info given to it by the system. + Details DocText `xml:"details"` +} + +// Format: The specification of an instruction encoding format. +type Format struct { + XMLName xml.Name + + // Id: The unique identifier of an instruction encoding format. + Id string `xml:"id,attr"` + + // DisplayName: The name of the instruction encoding format as it will be displayed in the documentation. + DisplayName string `xml:"display-name"` + + Description DocText `xml:"description"` + + Fields FormatFieldList `xml:"fields"` +} + +// FormatFieldList: The fields used in this instruction encoding format. +type FormatFieldList struct { + XMLName xml.Name + + Field []FormatField `xml:",any"` +} + +// FormatField: Description of a field in this instruction encoding format. +type FormatField struct { + XMLName xml.Name + + // Bits: Field length, in bits. + Bits int64 `xml:"bits,attr"` + + Type FormatFieldType `xml:"type,attr"` + + // Name: Field name. Ignored for opcode and condition fields. + Name string `xml:"name,attr,omitempty"` +} + +// Instruction: The specification of an instruction. +type Instruction struct { + XMLName xml.Name + + // Id: The unique identifier of this instruction. + Id string `xml:"id,attr"` + + // Opcode: The opcode of this instruction. + Opcode uint8 `xml:"opcode,attr"` + + // Format: The encoding format of the instruction. + Format string `xml:"format,attr"` + + // DisplayName: The name of the instruction as it will be displayed in the documentation. + DisplayName string `xml:"display-name"` + + // Description: The description of this instruction. + Description DocText `xml:"description"` + + Syntax Syntax `xml:"syntax"` + + Examples ExampleList `xml:"examples"` + + Bindings BindingList `xml:"bindings"` + + Operation Operation `xml:"operation"` + + Exceptions InstructionExceptionList `xml:"exceptions"` +} + +// Syntax: The assembler syntax of the instruction. +type Syntax struct { + XMLName xml.Name + + Mnemonic Mnemonic `xml:",any"` +} + +// MnemonicArgRegister: A register passed as an argument of the instruction. +type MnemonicArgRegister struct { + XMLName xml.Name + + Binding BindingName `xml:"binding,attr"` + + Ref []RefText `xml:"ref"` + + Todo []TodoText `xml:"todo"` +} + +// MnemonicArgImmediate: An immediate value passed as an argument of the instruction. +type MnemonicArgImmediate struct { + XMLName xml.Name + + Ref BindingName `xml:"ref,attr"` + + RefElm []RefText `xml:"ref"` + + Todo []TodoText `xml:"todo"` +} + +// MnemonicArgGroup: A group of both registers and immediate values passed as arguments of the instruction. +type MnemonicArgGroup struct { + XMLName xml.Name + + Register []MnemonicArgRegister `xml:"register"` + + Immediate []MnemonicArgImmediate `xml:"immediate"` +} + +// ExampleList: Instruction syntax examples. +type ExampleList struct { + XMLName xml.Name + + Example []Example `xml:",any"` +} + +// Example: An example of the instruction syntax. +type Example struct { + XMLName xml.Name + + Code Code `xml:"code"` + + Comment CodeComment `xml:"comment"` +} + +// CodeComment: Comment about the assembly code of the example. +type CodeComment struct { + XMLName xml.Name + + Ref []RefText `xml:"ref"` + + Todo []TodoText `xml:"todo"` +} + +// BindingList: Value bindings to instruction fields. +type BindingList struct { + XMLName xml.Name + + Binding []Binding `xml:",any"` +} + +// Binding: A value binding to an instruction field. +type Binding struct { + XMLName xml.Name + + Name BindingName `xml:"name,attr"` + + // Field: Reference to the instruction field to bind the value to. + Field string `xml:"field,attr"` + + // Aligned: Optional immediate value alignement, must be a power of 2. Dictates how the value is encoded: an alignment of 2 means that the value is bitshifted right by one bit, an alignment of 4 means that it is bitshifted right by two bits, etc... Defaults to 1, i.e. no bitshifting is involved. + Aligned uint64 `xml:"aligned,attr"` + + // MinusOne: Specifies whether the value is "-1 encoded". As an example, if an 8-bits value is -1 encoded then it means that one can use values in the range 1..256 and it will be encoded in the range 0..255. Defaults to false. + MinusOne bool `xml:"minus-one,attr"` + + // Signed: Indicate whether the immediate value is signed. Defaults to false. + Signed bool `xml:"signed,attr"` +} + +// InstructionExceptionList: The exceptions that can be generated during execution of this instruction. +type InstructionExceptionList struct { + XMLName xml.Name + + Exception []InstructionException `xml:",any"` +} + +// InstructionException: An exception that can be generated during the execution of this instruction. +type InstructionException struct { + XMLName xml.Name + + // Ref: The unique identifier of the exception. + Ref string `xml:"ref,attr"` + + RefElm []RefText `xml:"ref"` + + Todo []TodoText `xml:"todo"` +} + +// XSD SimpleType declarations + +// FormatFieldType: Field type. +type FormatFieldType string + +const FormatFieldTypeOpcode FormatFieldType = "opcode" + +const FormatFieldTypeRegister FormatFieldType = "register" + +const FormatFieldTypeRegisterSrc FormatFieldType = "register.src" + +const FormatFieldTypeRegisterDest FormatFieldType = "register.dest" + +const FormatFieldTypeRegisterOffset FormatFieldType = "register.offset" + +const FormatFieldTypeImmediate FormatFieldType = "immediate" + +const FormatFieldTypeImmediateOffset FormatFieldType = "immediate.offset" + +const FormatFieldTypeCondition FormatFieldType = "condition" + +const FormatFieldTypeFlag FormatFieldType = "flag" + +const FormatFieldTypeUnused FormatFieldType = "unused" + +// Mnemonic: Instruction mnemonic. +type Mnemonic string + +// Code: Single-line assembly code of the example. +type Code string + +// BindingName: The name of the value binding. +type BindingName string + +// Operation: Instruction microcoded operation. +type Operation string diff --git a/spec/visitor.go b/spec/visitor.go new file mode 100644 index 0000000..3db6792 --- /dev/null +++ b/spec/visitor.go @@ -0,0 +1,5 @@ +package spec + +type SpecVisitor interface { + VisitSpecification(node *Specification) +}