hi, I have a question about usage of enums to model a tree structure (e.g. AST).
type
NodeKind* = enum
Sheet,
Comment,
Rule,
AtRule,
Decl
Node* = ref NodeObj
NodeObj* {.acyclic.} = object
case kind*: NodeKind
of Sheet:
nodes*: seq[Node]
of Comment:
value*: string
of Rule:
selector*: string
nodes*: seq[Node]
of AtRule:
name*: string
params*: string
nodes*: seq[Node]
of Decl:
prop*: string
value*: string
This seem to be the pattern used on nim ast itself, my question is what would be the idiomatic way to restrict a type declaration to a subset of the Node enum. (e.g. nodes of Sheet can only be of kind Rule, Comment, and AtRule, nodes of Rule can only be of kind Decl, Comment, and AtRule, etc.
In Rust this is a known limitation for example and you end up needing to create auxiliary types (e.g. RuleChild, RootChild, etc.) leading to a quite ugly typing and very annoying usage and matching (partially sweetened by a good set of macros...). Was wandering if nim has the same limitation (as the size of the node must be known at compile time), or if there are better ways to model these pretty common scenarios.
Thanks
You might be able to do this with concepts. They let you add arbitrary constraints to a generic type. I’m not sure if it’d work while still resolving to the base Node type for storage.
Was wandering if nim has the same limitation (as the size of the node must be known at compile time)
This isn't true, you can just set the type size to the maximum of all the branch sizes.
I can only think of black magic ways to truly implement this and even then there are problems like generics being too limited.
For a start something like this compiles (the 1/2/3 suffixes are due to fields with same name not being allowed in different object variant branches, you can easily make a macro that lifts the branch fields to their own object type though, I should have one here):
type
NodeKind* = enum
Sheet,
Comment,
Rule,
AtRule,
Decl
NodeOf*[Kinds: static set[NodeKind]] = Node
Node* = ref NodeObj
NodeObj* {.acyclic.} = object
case kind*: NodeKind
of Sheet:
nodes1*: seq[NodeOf[{Rule, Comment, AtRule}]]
of Comment:
value1*: string
of Rule:
selector*: string
nodes2*: seq[NodeOf[{Decl, Comment, AtRule}]]
of AtRule:
name*: string
params*: string
nodes3*: seq[Node]
of Decl:
prop*: string
value2*: string
This works as documentation. We can do something like this to add type safety, although it will become verbose:
type
NodeKind* = enum
Sheet,
Comment,
Rule,
AtRule,
Decl
NodeOf*[Kinds: static set[NodeKind]] = distinct Node
Node* = ref NodeObj
NodeObj* {.acyclic.} = object
case kind*: NodeKind
of Sheet:
nodes1*: seq[NodeOf[{Rule, Comment, AtRule}]]
of Comment:
value1*: string
of Rule:
selector*: string
nodes2*: seq[NodeOf[{Decl, Comment, AtRule}]]
of AtRule:
name*: string
params*: string
nodes3*: seq[Node]
of Decl:
prop*: string
value2*: string
proc toNodeOf[Kinds: static set[NodeKind]](n: Node): NodeOf[Kinds] =
assert n.kind in Kinds
NodeOf[Kinds](n)
proc toNode[Kinds: static set[NodeKind]](n: NodeOf[Kinds]): Node = Node(n)
let n1 = Node(kind: Sheet)
let comment = Node(kind: Comment, value1: "coment")
n1.nodes1.add(toNodeOf[{Rule, Comment, AtRule}](comment))
let n2 = Node(kind: AtRule)
n2.nodes3.add(toNode(n1.nodes1[0]))
This does not generally work with converters, due to the static parameter we are using.