Skip to content

DQL NodeSet Implementation Specification

xushiwei edited this page Feb 13, 2026 · 3 revisions

This document specifies the implementation requirements for NodeSet types to support DQL (DOM Query Language) syntax in XGo. A NodeSet implementation must provide a set of specially-named methods that the XGo compiler recognizes and translates from the declarative DQL syntax.

Core Infrastructure

Required Methods

Any type implementing NodeSet must provide the following core methods:

XGo_Enum() iter.Seq[NodeSet]

Purpose: Enable iteration over the NodeSet

Semantics:

  • Returns an iterator sequence over the NodeSet
  • Each yielded value is a NodeSet (typically containing a single node), not a raw node
  • This preserves NodeSet syntax capabilities within iteration variables

DQL Usage:

// for..in loop
for user in doc.users.* { ... }

// List comprehension
names := [user.$name for user in doc.users.*]

Compiler Translation:

// for user in doc.users.* { ... }
// Compiles to:
for user in doc.XGo_Elem("users").XGo_Child().XGo_Enum() { ... }

Requirements:

  • Must return iter.Seq[NodeSet], not iter.Seq[Node]
  • Each yielded NodeSet typically contains a single node
  • Should support lazy iteration where possible
  • Must handle empty NodeSets gracefully (yields nothing)
  • Must respect early termination (when iterator consumer returns false)

XGo_first() (NodeType, error)

Purpose: Extract the first node from a NodeSet

DQL Syntax: ns._first

Semantics:

  • Returns the first (or only) node from the NodeSet
  • Returns error if NodeSet is empty or has internal error state
  • Used both by users directly and internally by compiler in select operation transformations

DQL Usage:

// User can call directly
node, err := ns._first

// Also used by compiler in conditional select transformations

Compiler Translation:

// DQL: node, err := doc.users.*._first
// Compiles to: node, err := doc.XGo_Elem("users").XGo_Child().XGo_first()

// Used internally in conditional select:
if __xgo_val, __xgo_err := self.XGo_first(); __xgo_err == nil {
    if !__xgo_yield(__xgo_val) {
        return false
    }
}

Requirements:

  • Must return the first node in the set
  • Must return dql.ErrNotFound if NodeSet is empty
  • Must propagate internal error state if present
  • NodeType is domain-specific (the actual node type for this implementation)
  • Should be efficient (O(1) if possible)
  • Should provide both single-value and dual-value versions like other methods, if appropriate for the domain

Type Conversion Function

NodeSet_Cast(seq func(func(NodeType) bool)) NodeSet

Purpose: Enable XGo type conversion syntax for constructing NodeSet from sequence functions

XGo Type Conversion Syntax:

In XGo, if a function named TypeName_Cast(SourceType) TypeName exists, users can use the type conversion syntax:

result := TypeName(sourceValue)  // Equivalent to TypeName_Cast(sourceValue)

For NodeSet, this means:

// If this function exists:
func NodeSet_Cast(seq func(func(NodeType) bool)) NodeSet

// Users can write:
ns := NodeSet(mySequence)  // Converts sequence to NodeSet

Semantics:

  • Takes a sequence function that yields nodes via a callback
  • Returns a NodeSet containing the yielded nodes
  • This is a public API that users can directly use
  • Also used internally by the compiler for conditional select operations

User-Facing Usage:

// User creates a sequence function
mySeq := func(yield func(NodeType) bool) {
    // Generate nodes...
    for _, node := range someNodes {
        if !yield(node) {
            return  // Early termination
        }
    }
}

// User converts it to NodeSet using type conversion syntax
ns := NodeSet(mySeq)

// Now can use DQL syntax on ns
name := ns.$name
for child in ns.* { ... }

Compiler-Generated Usage:

// DQL: users@($age < 18)
// Compiles to:
NodeSet(func(__xgo_yield func(NodeType) bool) {
    users.XGo_Enum()(func(self NodeSet) bool {
        if self.XGo_Attr__0("age") < 18 {
            if __xgo_val, __xgo_err := self.XGo_first(); __xgo_err == nil {
                if !__xgo_yield(__xgo_val) {
                    return false
                }
            }
        }
        return true
    })
})

Requirements:

  • Must construct a valid NodeSet from the sequence function
  • Should preserve lazy evaluation if possible
  • Must handle early termination correctly (when yield returns false)
  • NodeType is domain-specific
  • Must be exported (public) so users can leverage the type conversion syntax

When Users Might Use This:

  • Integrating external iterators or generators with DQL
  • Building custom NodeSet sources from application logic
  • Converting between different sequence representations
  • Implementing adapters for third-party tree structures

Child Node Access

XGo_Elem(name string) NodeSet

DQL Syntax: ns.name or ns."elem-name"

Semantics:

  • Returns a NodeSet containing all direct children of nodes in ns with the specified name
  • For quoted names like ns."elem-name", the compiler passes "elem-name" as the name parameter

Examples:

// DQL: doc.users.profile
// Compiles to: doc.XGo_Elem("users").XGo_Elem("profile")

// DQL: doc."user-data".profile
// Compiles to: doc.XGo_Elem("user-data").XGo_Elem("profile")

Requirements:

  • Must handle empty NodeSets gracefully (return empty NodeSet)
  • Should support lazy evaluation where possible
  • Must preserve internal error state from parent NodeSet

XGo_Child() NodeSet

DQL Syntax: ns.*

Semantics:

  • Returns a NodeSet containing all direct children of all nodes in ns
  • Wildcard selector - does not filter by name

Examples:

// DQL: doc.users.*
// Compiles to: doc.XGo_Elem("users").XGo_Child()

Requirements:

  • Must include all child nodes regardless of name
  • Order may be significant depending on the domain (e.g., document order in XML/HTML)
  • Should support lazy evaluation

XGo_Index(index IndexType) NodeSet

DQL Syntax: ns[index]

Semantics:

  • Returns a NodeSet containing the child node at the specified index
  • Uses 0-based indexing
  • IndexType is typically int but may be domain-specific

Examples:

// DQL: doc.users[0]
// Compiles to: doc.XGo_Elem("users").XGo_Index(0)

// DQL: doc.users[2].name
// Compiles to: doc.XGo_Elem("users").XGo_Index(2).XGo_Elem("name")

// DQL: doc.users[0].$name
// Compiles to: doc.XGo_Elem("users").XGo_Index(0).XGo_Attr__0("name")

Requirements:

  • Must handle out-of-bounds indices gracefully (return empty NodeSet or NodeSet with error state)
  • Index semantics should match the domain (e.g., array index for JSON, child position for XML)
  • Should work on both single nodes and multi-node sets (applies index to each node's children)

XGo_Any(name string) NodeSet

DQL Syntax: ns.**.name, ns.**."elem-name", or ns.**.*

Semantics:

  • Deep/recursive query: searches for nodes at any depth within ns
  • When name is non-empty: returns all descendant nodes with that name
  • When name is empty string "": returns all descendant nodes (for ns.**.* syntax)

Examples:

// DQL: doc.**.div
// Compiles to: doc.XGo_Any("div")

// DQL: doc.**."elem-name"
// Compiles to: doc.XGo_Any("elem-name")

// DQL: doc.**.*
// Compiles to: doc.XGo_Any("")

Requirements:

  • Must search recursively through entire subtree
  • Should support lazy evaluation to avoid materializing entire tree
  • May use depth-first or breadth-first traversal (domain-specific choice)
  • Empty string parameter means "match all descendants"

Node Filtering

XGo_Select(name string) NodeSet

DQL Syntax: ns@name or ns@"elem-name"

Semantics:

  • Filters nodes in ns to only those with the specified name
  • Simplified form of select operation for name-based filtering

Examples:

// DQL: doc.users.*@admin
// Compiles to: doc.XGo_Elem("users").XGo_Child().XGo_Select("admin")

Requirements:

  • Should filter to nodes matching the given name
  • Must maintain NodeSet semantics (return empty set if no matches)
  • Should support lazy evaluation

Conditional Select (Compiler-Generated)

DQL Syntax: ns@(condExpr) or ns@fn(args)

Semantics:

  • Filters nodes based on arbitrary boolean condition
  • The compiler transforms this into NodeSet type conversion with generated filter logic

Compiler Transformation:

// DQL: users@($age < 18)
// Compiles to:
NodeSet(func(__xgo_yield func(NodeType) bool) {
    users.XGo_Enum()(func(self NodeSet) bool {
        // Condition evaluation depends on available version
        if self.XGo_Attr__0("age") < 18 {  // Single-value version
            if __xgo_val, __xgo_err := self.XGo_first(); __xgo_err == nil {
                if !__xgo_yield(__xgo_val) {
                    return false
                }
            }
        }
        return true
    })
})

Requirements:

  • NodeSet_Cast must be implemented to enable the NodeSet(seq) conversion syntax
  • XGo_first() must extract the first node from a NodeSet
  • The pattern ensures each node in the iteration is still a NodeSet (single-element), preserving DQL syntax capabilities

Attribute Access

Two-Version Design

DQL provides two versions for attribute access, similar to Go's map[key] pattern:

  1. Single-value version: XGo_Attr__0(name string) ValueType

    • Returns only the value
    • Defaults to zero value if error occurs
    • Convenient when errors can be safely ignored
  2. Dual-value version: XGo_Attr__1(name string) (ValueType, error)

    • Returns both value and error
    • Allows explicit error handling

XGo_Attr__0(name string) ValueType

DQL Syntax: ns.$name or ns.$"attr-name" (single-value version)

Semantics:

  • Returns the attribute value from the first node in the NodeSet
  • If error occurs (empty NodeSet, missing attribute, internal error state), returns zero value of ValueType
  • Equivalent to ns.XGo_Attr__1(name)?:zeroValue

Examples:

// DQL: user.$name
// Compiles to: user.XGo_Attr__0("name")

// DQL: user.$"attr-name"
// Compiles to: user.XGo_Attr__0("attr-name")

// DQL: names := [user.$name for user in doc.users.*]
// Single-value version in list comprehension

Requirements:

  • Must operate on first node only
  • Must return zero value on any error (node not found, attribute missing, type conversion failure, internal error state)
  • ValueType is domain-specific (often string, any, or specific type)

XGo_Attr__1(name string) (ValueType, error)

DQL Syntax: val, err := ns.$name or val, err := ns.$"attr-name" (dual-value version)

Semantics:

  • Returns the attribute value and error from the first node in the NodeSet
  • Allows explicit error handling

Examples:

// DQL: name, err := user.$name
// Compiles to: name, err := user.XGo_Attr__1("name")

// DQL: names := [user.$name! for user in doc.users.*]
// With error operator (panics if error)

// DQL: names := [user.$name?:"N/A" for user in doc.users.*]
// With default value operator

Requirements:

  • Must operate on first node only
  • Should return appropriate errors for:
    • Empty NodeSet
    • Missing attribute
    • Type conversion failure
    • Internal error state (e.g., from XGo_single())
  • ValueType is domain-specific

Method Access

Regular Methods

DQL Syntax: ns.method(args)

Semantics:

  • Regular XGo method calls on NodeSet
  • Method name is used as-is
  • Typically operates on first node in NodeSet (implementation should document this)

Examples:

// DQL: div.text
// Compiles to: div.Text() (assuming parentheses can be omitted)

// DQL: div.text(true)
// Compiles to: div.Text(true)

// DQL: node.count
// Compiles to: node.Count()

Two-Version Design:

Most methods should provide two versions:

  1. Single-value version: Method(args) ResultType

    • Returns only the result
    • Defaults to zero value if error occurs
    • Exception: Methods returning numeric or sensitive types may provide only dual-value version to prevent subtle bugs
  2. Dual-value version: Method(args) (ResultType, error)

    • Returns both result and error
    • Allows explicit error handling

Requirements:

  • Follow standard XGo method conventions
  • Most methods should provide both single-value and dual-value versions
  • Methods returning numeric types (where zero-default could cause bugs) should provide only dual-value version
  • Should document whether they operate on first node or entire NodeSet
  • Can integrate with XGo's error handling operators (! and ?:)

Prefixed Methods

DQL Syntax: ns._method (without parentheses) or ns.XGo_method() (with parentheses)

Semantics:

  • Method calls with _ prefix map to XGo_method function
  • Used for general-form method access or to avoid naming conflicts with child nodes
  • When using the _method syntax (without parentheses), XGo applies the function call omission feature
  • When using parentheses, the full method name XGo_method() must be used

Examples:

// DQL: node._text (without parentheses)
// Compiles to: node.XGo_text()

// DQL: node.XGo_text() (with parentheses)
// Compiles to: node.XGo_text()

// DQL: node._count
// Compiles to: node.XGo_count()

// DQL: node._all
// Compiles to: node.XGo_all()

// DQL: node._first
// Compiles to: node.XGo_first()

Naming Pattern:

  • Function name is XGo_ + method name
  • Example: _textXGo_text(), _countXGo_count(), _firstXGo_first()

Requirements:

  • Otherwise follows same conventions as regular methods
  • Should provide two-version design where appropriate
  • Useful when child node names might conflict with method names

Special Case - Child Nodes Starting with _:

// DQL: node."_text"
// Compiles to: node.XGo_Elem("_text")
// Accesses child node named "_text"

// DQL: node._text
// Compiles to: node.XGo_text()
// Calls XGo_text() method

Cache and Query Control Methods

These methods provide fine-grained control over query evaluation and caching. The specific method names used depend on the NodeSet implementation's choice.

Cache Method Naming

Implementations may provide cache control methods using either:

  • General form: XGo_all(), XGo_one(), XGo_single()
  • Domain-specific form: All(), One(), Single() (or other names)

The choice depends on:

  • Whether the domain has existing methods with these names
  • Domain-specific naming conventions
  • Whether there are potential naming conflicts

DQL Syntax Mapping:

  • If implementation provides XGo_all(): use ns._all in DQL
  • If implementation provides All(): use ns.all in DQL
  • Similarly for _one/one and _single/single

All/XGo_all - Materialize and Cache

DQL Syntax: ns.all or ns._all (depending on implementation)

Method Signature:

// Domain-specific form
func (ns NodeSet) All() NodeSet

// Or general form
func (ns NodeSet) XGo_all() NodeSet

Semantics:

  • Executes the query immediately and materializes all matching nodes
  • Returns a new NodeSet containing cached results
  • Subsequent operations on returned NodeSet don't re-execute the original query
  • The original NodeSet remains unchanged (still lazy)

Examples:

// If implementation provides All():
// DQL: users := doc.users.*@($active == true).all
// Compiles to: users := doc.XGo_Elem("users").XGo_Child().[filter].All()

// If implementation provides XGo_all():
// DQL: users := doc.users.*@($active == true)._all
// Compiles to: users := doc.XGo_Elem("users").XGo_Child().[filter].XGo_all()

Requirements:

  • Must fully evaluate the lazy query
  • Must cache results in the returned NodeSet
  • Original NodeSet should remain lazy
  • Should handle large result sets efficiently
  • Must preserve error state if present

When to Use:

  • NodeSet will be accessed multiple times
  • Multiple attributes/methods will be extracted from same set
  • Query is expensive and results are reused
  • Working with reasonably sized result set that fits in memory

One/XGo_one - Early Termination

DQL Syntax: ns.one or ns._one (depending on implementation)

Method Signature:

// Domain-specific form
func (ns NodeSet) One() NodeSet

// Or general form
func (ns NodeSet) XGo_one() NodeSet

Semantics:

  • Optimizes queries when expecting only one node, allowing early termination
  • Returns a NodeSet containing the first match
  • If no nodes match: returns NodeSet marked internally with dql.ErrNotFound error
  • If multiple nodes match: returns first match (does NOT validate uniqueness)
  • The returned NodeSet can be used normally (attributes, methods, iteration)

Examples:

// If implementation provides One():
// DQL: admin := doc.users.*@($role == "admin").one
// Compiles to: admin := doc.XGo_Elem("users").XGo_Child().[filter].One()

// If implementation provides XGo_one():
// DQL: admin := doc.users.*@($role == "admin")._one
// Compiles to: admin := doc.XGo_Elem("users").XGo_Child().[filter].XGo_one()

Requirements:

  • Must stop query execution after finding first match
  • If no matches, must return NodeSet with dql.ErrNotFound internal error
  • Returned NodeSet should be cached (single node)
  • Should NOT check for multiple matches (that's single's job)
  • Internal error must propagate to all attribute/method calls on returned NodeSet

When to Use:

  • Expecting only one node match
  • Performance optimization for single-result queries
  • Early exit from expensive deep queries
  • Existence checks where expecting at most one match

Error Propagation:

// Error propagates through attribute access
name := admin.$name      // Returns zero value if dql.ErrNotFound
name, err := admin.$name // err == dql.ErrNotFound if no match

// Or using error operators
name := admin.$name!              // Panics if dql.ErrNotFound
name := admin.$name?:"Default"    // Uses default if dql.ErrNotFound

Single/XGo_single - Enforce Uniqueness

DQL Syntax: ns.single or ns._single (depending on implementation)

Method Signature:

// Domain-specific form
func (ns NodeSet) Single() NodeSet

// Or general form
func (ns NodeSet) XGo_single() NodeSet

Semantics:

  • Validates that query matches exactly one node
  • Returns NodeSet containing the single matching node if exactly one match
  • If 0 matches: returns NodeSet marked internally with dql.ErrNotFound error
  • If >1 matches: returns NodeSet marked internally with dql.ErrMultipleEntities error
  • The returned NodeSet can still be used (attributes, methods, iteration) but carries error state

Examples:

// If implementation provides Single():
// DQL: user := doc.users.*@($id == 12345).single
// Compiles to: user := doc.XGo_Elem("users").XGo_Child().[filter].Single()

// If implementation provides XGo_single():
// DQL: user := doc.users.*@($id == 12345)._single
// Compiles to: user := doc.XGo_Elem("users").XGo_Child().[filter].XGo_single()

Requirements:

  • Must validate uniqueness constraint (scan enough to determine 0, 1, or >1 matches)
  • If zero matches, must return NodeSet with dql.ErrNotFound internal error
  • If multiple matches, must return NodeSet with dql.ErrMultipleEntities internal error
  • If exactly one match, returned NodeSet should be cached
  • Internal error must propagate to all attribute/method calls
  • Should optimize to stop after finding 2 nodes (sufficient to detect multiplicity)

When to Use:

  • Require exactly one match and need validation
  • Working with unique identifiers (IDs, primary keys)
  • Validating data integrity and consistency
  • Preventing silent logic errors from unexpected multiple matches
  • Configuration where exactly one value must exist

Error Handling:

// Error propagates to attribute access
name, err := user.$name
if err == dql.ErrNotFound {
    // Handle missing case
} else if err == dql.ErrMultipleEntities {
    // Handle duplicate case (data integrity issue)
}

// Using panic operator when violations are bugs
config := doc.config.*@($key == "api_endpoint")._single
endpoint := config.$value!  // Panics if dql.ErrNotFound or dql.ErrMultipleEntities

Comparison with One:

// one: assumes single result, stops at first match (no validation)
admin := doc.users.*@($role == "admin").one
name := admin.$name  // If 3 admins exist: returns first admin's name

// single: validates single result, checks for multiple
admin := doc.users.*@($role == "admin").single
name, err := admin.$name  // If 3 admins exist: err == dql.ErrMultipleEntities

Standard Error Types

The DQL package provides standard error types that implementations should use:

package dql

var (
    ErrNotFound         error = errors.New("node not found")
    ErrMultipleEntities error = errors.New("multiple entities found, expected single")
)

These errors are used by:

  • XGo_first(): returns dql.ErrNotFound when no matches
  • XGo_one() / One(): returns dql.ErrNotFound when no matches
  • XGo_single() / Single(): returns dql.ErrNotFound (0 matches) or dql.ErrMultipleEntities (>1 matches)

Error State Propagation

NodeSet implementations should support internal error state:

Concept:

  • A NodeSet can carry an internal error (e.g., from one, single)
  • When NodeSet has internal error, all attribute and method calls return that error

Pattern:

// NodeSet with internal error
ns := doc.users.*@($id == 999)._one  // Returns NodeSet marked with dql.ErrNotFound

// Error propagates automatically
name, err := ns.$name  // err == dql.ErrNotFound
age := ns.$age!        // Panics with dql.ErrNotFound

// Can still use NodeSet normally
for user in ns {  // Loop doesn't execute (empty due to error)
    // ...
}

Benefits:

  • Eliminates need for dual-value versions of cache methods
  • Errors propagate naturally through query pipeline
  • Consistent error handling pattern

Type Parameters and Domain-Specific Customization

IndexType

The IndexType in XGo_Index is typically int but can be customized:

// Typical: integer indexing
func (ns NodeSet) XGo_Index(index int) NodeSet

// Alternative: string keys for map-like structures
func (ns NodeSet) XGo_Index(index string) NodeSet

ValueType

The ValueType in attribute methods is domain-specific:

// String-centric
func (ns NodeSet) XGo_Attr__0(name string) string

// Generic
func (ns NodeSet) XGo_Attr__0(name string) any

// Typed
func (ns NodeSet) XGo_Attr__0(name string) ValueType

NodeType

The NodeType in XGo_first() and NodeSet_Cast is domain-specific:

// HTML DOM
func (ns NodeSet) XGo_first() (*HTMLNode, error)
func NodeSet_Cast(seq func(func(*HTMLNode) bool)) NodeSet

// JSON DOM
func (ns NodeSet) XGo_first() (*JSONNode, error)
func NodeSet_Cast(seq func(func(*JSONNode) bool)) NodeSet

Performance Considerations

Lazy Evaluation

Most operations should use lazy evaluation:

  • Return lightweight query descriptors, not materialized results
  • Defer actual tree traversal until iteration or attribute access
  • Enable query optimization through plan analysis

Early Termination

Support early termination in iterators:

  • Respect the boolean return value from yield functions
  • Stop processing when consumer returns false
  • Optimize one to stop after first match
  • Optimize single to stop after finding 2 nodes

Caching Strategy

Implement caching efficiently:

  • all: Materialize and store all nodes
  • one: Stop after first match, cache single node
  • single: Stop after finding 2 nodes (sufficient to detect multiplicity), cache if exactly one

DQL Syntax to Method Mapping Reference

DQL Syntax Method Signature Notes
ns.name XGo_Elem(name string) NodeSet Access child nodes
ns."elem-name" XGo_Elem("elem-name") NodeSet Special characters in name
ns.* XGo_Child() NodeSet All child nodes
ns[index] XGo_Index(index IndexType) NodeSet Index access (0-based)
ns.**.name XGo_Any("name") NodeSet Deep query
ns.**.* XGo_Any("") NodeSet All descendants
ns@name XGo_Select(name string) NodeSet Filter by name
ns@(cond) Compiler-generated + NodeSet(seq) Conditional filter via type conversion
ns.$name XGo_Attr__0(name string) ValueType Attribute (single-value)
val, err := ns.$name XGo_Attr__1(name string) (ValueType, error) Attribute (dual-value)
ns.method(args) Method(args) or XGo_method(args) Regular method call
ns._method XGo_method() Prefixed method (no parens)
ns.XGo_method() XGo_method() Prefixed method (with parens)
ns._first XGo_first() (NodeType, error) Extract first node (user-accessible, no parens)
ns.XGo_first() XGo_first() (NodeType, error) Extract first node (with parens)
ns.all All() NodeSet Cache all (if provided)
ns._all XGo_all() NodeSet Cache all (general form)
ns.one One() NodeSet First match (if provided)
ns._one XGo_one() NodeSet First match (general form)
ns.single Single() NodeSet Unique match (if provided)
ns._single XGo_single() NodeSet Unique match (general form)
for x in ns XGo_Enum() iter.Seq[NodeSet] Iteration support
NodeSet(seq) NodeSet_Cast(func(func(NodeType) bool)) NodeSet Type conversion from sequence

Summary

This specification defines the contract for NodeSet implementations in DQL:

  1. Core Infrastructure: XGo_Enum(), XGo_first(), NodeSet_Cast() (for type conversion)
  2. Child Access: XGo_Elem(), XGo_Child(), XGo_Index(), XGo_Any()
  3. Filtering: XGo_Select() and compiler-generated conditional filters using NodeSet(seq) conversion
  4. Attributes: Two-version design with XGo_Attr__0() and XGo_Attr__1()
  5. Methods: Regular methods and XGo_method() prefixed form (including XGo_first() which is user-accessible via _first syntax)
  6. Cache Control: Implementation chooses between general form (XGo_all/one/single) or domain-specific form (All/One/Single)
  7. Error Handling: Internal error state with standard dql.ErrNotFound and dql.ErrMultipleEntities from the dql package
  8. Type Customization: Domain-specific NodeType, ValueType, IndexType

Implementations should adapt type parameters to their specific domain while maintaining these core semantics and providing efficient lazy evaluation with appropriate caching strategies.

Clone this wiki locally