Skip to content

proposal: standardize classfile resource semantics #2704

Description

@aofei

Summary

This proposal standardizes source-level resource semantics for XGo classfile frameworks.

It introduces:

  • framework-declared resource kinds
  • canonical string-based resource reference types
  • optional handle-bearing types for top-level resource kinds
  • active-project-group pack documents as the standardized discovery substrate
  • discovery-based concrete resource introduction over pack documents
  • typed API-position scope bindings for scoped canonical resource references
  • one narrow work-classfile implication rule for top-level resource identities
  • shared static semantics for resource references, completion, hover, rename, and diagnostics

Problem

Today, tool support for classfile-framework resources is framework-specific and mostly hardcoded.

For example, a tool such as xgolsw can provide good support for spx resources only because it already knows all of the following:

  • which types mean sprite, sound, backdrop, widget, and scoped sprite resources
  • how one active project group derives its pack root and pack document
  • how to discover concrete instances from pack-document object trees rooted at framework-declared config directories
  • how to infer the scope of SpriteCostumeName or SpriteCostumeFrameName from the current sprite context

This does not scale across frameworks.

Goals

  • Let one framework declare its resource schema in framework source code
  • Let tooling obtain concrete resource instances without executing arbitrary framework code
  • Keep resource identity stable and independent from runtime object identity or storage paths
  • Make scoped resource discovery natural for frameworks such as spx
  • Support precise source-level resource semantics for canonical typed references
  • Let tooling obtain explicit scope for scoped API positions when type information alone is insufficient
  • Keep the model small enough that different frameworks can realistically adopt it

Non-goals

  • Standardize runtime resource loading or runtime lookup keys
  • Standardize one whole-project file-tree document as the discovery substrate
  • Standardize declaration-based resource introduction for framework-declared named values such as Direction or EffectKind
  • Standardize project-class implied resources
  • Standardize scoped classfile-implied resources beyond one narrow work-classfile rule
  • Standardize API-position kind binding for plain string parameters
  • Replace editor-specific input-slot or input-type protocols
  • Turn DQL into a general-purpose result-construction language

Proposed model

1. Resource kinds and type bindings

Framework source declarations may declare resource kinds through source-attached directive comments:

//xgo:class:resource sprite
type SpriteName = string

type (
    //xgo:class:resource sprite.costume
    SpriteCostumeName = string
)

//xgo:class:resource sprite
type Sprite interface { Name() string }

These comments attach to one top-level type spec. That includes one type spec inside a grouped type (...) declaration when the comment belongs to that specific type spec. It does not include one grouped type declaration as a whole.

This proposal uses dotted resource-kind spelling to encode kind hierarchy directly:

  • sprite is top-level
  • sprite.costume is scoped under sprite
  • sprite.costume.frame is scoped under sprite.costume

The //xgo:class:resource <kind> form means one of the following:

  • one string-based type declares the canonical resource reference type of one kind
  • one top-level kind may additionally declare one or more handle-bearing interface or struct types

2. Pack roots and pack documents

This proposal depends on one pack document derived per active project group at the classfile-spec layer.

One active project group may declare one pack directive in gox.mod:

pack assets index.json

That directive determines:

  • one pack root directory, resolved from the named relative directory under the project root directory
  • one exact index file name, such as index.json, index.yml, or index.yaml

The pack document is the logical merged object derived from that pack root and that exact index file name:

  • the root object is parsed from R/F
  • each descendant directory of R that also contains F contributes one child object
  • that child object is merged at its relative directory path from R
  • intermediate objects are created as needed
  • files other than contributing F files are outside the standardized pack-document model

One spx-style pack document may look like:

{
  "zorder": ["Hero", "Enemy"],
  "sprites": {
    "Hero": {
      "costumes": [
        {"name": "idle", "frames": [{"name": "f1"}, {"name": "f2"}]},
        {"name": "run", "frames": [{"name": "f1"}]}
      ]
    },
    "Enemy": {
      "costumes": [
        {"name": "idle", "frames": [{"name": "f1"}]}
      ]
    }
  },
  "widgets": {
    "Score": {
      "id": "score-widget"
    }
  }
}

An implementation may materialize that document as index_pack.json, index_pack.yml, or index_pack.yaml, but the logical pack document is the standardized artifact.

3. Concrete resource introduction

Concrete resource introduction occurs in two forms:

  • discovery-based introduction over pack documents
  • one narrow work-classfile implication rule

3.1. Discovery-based introduction

Project-derived resource instances are declared on canonical resource reference types through:

//xgo:class:resource-discovery <DQL>
//xgo:class:resource-name-discovery <DQL>

resource-discovery selects discovery origin nodes.

resource-name-discovery, when present, resolves local names relative to those origin nodes.

For each discovery origin node, resource-name-discovery must resolve to exactly one matched node. The local resource name is then taken from:

  1. that node's string scalar value, if any
  2. otherwise that node's key name
  3. otherwise that node's string member name
  4. otherwise the candidate is invalid

Without resource-name-discovery, local names use a default extraction rule:

  1. discovery origin node key name
  2. otherwise its string member name
  3. otherwise the match is invalid

Example:

//xgo:class:resource sprite
//xgo:class:resource-discovery sprites.*
type SpriteName = string

//xgo:class:resource sprite.costume
//xgo:class:resource-discovery costumes.*
type SpriteCostumeName = string

//xgo:class:resource widget
//xgo:class:resource-discovery widgets.*
//xgo:class:resource-name-discovery id
type WidgetName = string

Discovery execution is intentionally relative:

  • top-level kinds run on the root of the active project group's pack document, if any
  • scoped kinds run relative to each discovered direct parent origin node
  • scoped-kind queries remain short and framework-authored, for example:
    • sprites.*
    • costumes.*
    • frames.*

On the example pack document above:

  • sprites.* on the pack-document root discovers the sprite origins Hero and Enemy
  • costumes.* relative to the Hero sprite origin discovers idle and run
  • frames.* relative to the idle costume origin discovers f1 and f2

3.2. Work classfile implication

If the registered work base class declaration of one work file kind bears //xgo:class:resource <kind>, each work classfile of that kind implies one top-level resource identity whose local resource name is the class file stem.

This rule remains intentionally narrow:

  • it applies only to work classfiles
  • it applies only to one top-level kind
  • it does not create discovery origin nodes
  • it does not by itself create scoped child discovery roots

4. Typed resource references

This proposal keeps resource participation type-bound.

For canonical resource reference types:

  • ordinary Go typing determines whether a position participates in standardized resource semantics
  • string literals and statically evaluable string constants are resource reference candidates
  • lookup uses full resource identity, including scope chain when applicable
  • unresolved scope keeps the reference scope-unknown

5. Typed API-position scope bindings

Scoped canonical resource references may obtain explicit direct-parent scope through:

//xgo:class:resource-api-scope-binding <target> <source>

where:

  • <target> is one param.n
  • <source> is receiver or one param.n

These comments belong to one framework callable site:

  • one top-level function declaration
  • one top-level method declaration
  • one method spec declared by one top-level handle-bearing interface type declaration

Example:

//xgo:class:resource sprite
type Sprite interface {
    //xgo:class:resource-api-scope-binding param.0 receiver
    SetCostume__0(costume SpriteCostumeName)
}

//xgo:class:resource-api-scope-binding param.0 receiver
func (p *SpriteImpl) SetCostume__0(costume SpriteCostumeName)

//xgo:class:resource-api-scope-binding param.0 receiver
//xgo:class:resource-api-scope-binding param.1 param.0
func (p *SpriteImpl) SetCostumeAndFrame(costume SpriteCostumeName, frame SpriteCostumeFrameName)

This means:

  • costume gets its direct parent scope from the sprite receiver
  • frame gets its direct parent scope from the costume argument
  • the same binding model applies whether the callable site is one function declaration, one method declaration, or one
    interface method spec

6. Scoped owner inference

This proposal still keeps one narrow fallback inference rule for scoped canonical references inside work classfiles.

If all of the following hold:

  • the current source position is inside one work classfile
  • the registered work base class declaration bears a top-level resource comment for <parentKind>
  • the referenced scoped kind has direct parent kind <parentKind>
  • no explicit scope is otherwise available

then the direct parent identity is the implied top-level resource of the containing work classfile.

Why this shape

Why source-attached metadata

Resource kinds and introduction rules belong to framework declarations, not to gox.mod.

This keeps:

  • classfile registration metadata in gox.mod
  • resource schema metadata in framework source

Why pack documents instead of one whole-project file-tree document

The accepted pack direction already gives classfile frameworks one configuration substrate per active project group.

Using that substrate keeps discovery aligned with framework-authored config structure and avoids standardizing a separate whole-project tree model that is broader than current framework needs.

Why DQL only for discovery

DQL fits pack-document discovery well because it queries tree-shaped configuration data.

It does not fit API-position scope binding as well. API scope binding is a relation between:

  • receiver
  • parameter positions

That is better modeled as a small declarative relation set than as a synthetic query document.

Why typed references only

Canonical string-based resource reference types already provide an exact, portable hook for static semantics.

By contrast, plain string parameters mix many unrelated meanings:

  • route text
  • template names
  • command text
  • arbitrary framework-specific text

This proposal keeps the standardized model narrow and type-bound.

Why no project-class implied resources

Work classfiles have one clean naming source: the class file stem.

Project classes do not.

In particular:

  • explicit main project classfiles do not provide one useful semantic name
  • synthesized default project classes have no source file of their own

So project-class implied resources are intentionally left out.

Expected impact on tooling

For xgolsw-style tooling, this proposal replaces framework-specific hardcoded resource logic with declarative framework-authored metadata.

In practical terms, tooling gains:

  • one shared resource schema model across frameworks
  • one shared discovery execution model over active-project-group pack documents
  • one shared identity model for hover, completion, rename, references, and diagnostics
  • one shared way to obtain exact scope for typed scoped API positions

This still does not replace the full input-slot or input-type layer. It standardizes the semantic substrate that those tooling layers can consume.

Example: spx

Assume the active .spx project group in gox.mod declares:

pack assets index.json

Framework source declarations:

//xgo:class:resource sprite
//xgo:class:resource-discovery sprites.*
type SpriteName = string

//xgo:class:resource sprite.costume
//xgo:class:resource-discovery costumes.*
type SpriteCostumeName = string

//xgo:class:resource sprite.costume.frame
//xgo:class:resource-discovery frames.*
type SpriteCostumeFrameName = string

//xgo:class:resource sprite
type SpriteImpl struct{}

//xgo:class:resource-api-scope-binding param.0 receiver
func (p *SpriteImpl) SetCostume__0(costume SpriteCostumeName)

//xgo:class:resource-api-scope-binding param.0 receiver
//xgo:class:resource-api-scope-binding param.1 param.0
func (p *SpriteImpl) SetCostumeAndFrame(costume SpriteCostumeName, frame SpriteCostumeFrameName)

Then:

  • the active .spx project group derives its pack document from assets/index.json
  • sprites.* discovers sprite origins at the pack-document root
  • costumes.* discovers one sprite's costumes relative to that sprite origin
  • frames.* discovers one costume's frame resources relative to that costume origin
  • one call to SetCostume__0("idle") on Hero can resolve idle against scope (sprite, Hero)
  • one call to SetCostumeAndFrame("idle", "f1") on Hero can resolve "f1" against scope (sprite, Hero) + (sprite.costume, idle)

Deferred items

  • stronger framework-defined owner inference
  • one stronger standardized exactness model for missing-resource diagnostics

Metadata

Metadata

Assignees

Labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions