Given a simple "kinded" object like:
type
NodeKind = enum
nkParent,
nkChild
Node = ref NodeObj
NodeObj {.acyclic.} = object
case kind: NodeKind
of nkParent:
children: seq[Node]
of nkChild:
name: string
let a = Node(kind: nkParent)
a.name = "foo" # ideally this should not compile and the language server should not not provide "name" as an option
the program compiles correctly, but when it runs it throws an unhanded exception as name is not a valid field for a nkParent. Is there a clean recommended way to have this kind of type safety to work at compile time instead?
Also the language server provides name as a possible autocomplete as well, while it would be nice to have only suggestions present on that specific kind.
This seem to be a very basic expectation in terms of type safety. It is extremely easy to introduce bugs that can only be caught too late at runtime if this code compiles correctly. Maybe I am missing some core concepts here on how to idiomatically have type safety in similar use cases?
cool, I have 2 questions/issues though:
1. I was able to enable the warning using a pragma like {.warning[ProveField]: on.} ` on top of the file, but not sure how to pass it as a compilation argument. I tried nim c or nimble build with that argument and it says `no matches found: --warning[ProveField]:on. Also is there a way to enforce it on the nimble file for example?
2. It seems that the warning is raised for any field, even the one that are available under that kind. e.g.
let a = Node(kind: nkParent)
a.children = @[] # this should be ok, but it also raises a warning
a.name = "foo"
sorry if the code is not too clean, when I do
let a = Node(kind: nkParent, children: @[]) # this is fine (now warnings)
echo $a.children # this throws a warning (cannot prove that field is accessible)
Myabe I am missing something, but was expecting that once a is assigned to a node of that specific kind, then fields would be seen as "accessible", otherwise I can initialize the node, but have no way to later on access those fields, or know from the LSP which one are accessible based on the kind or not.
ok, this is working:
let a = Node(kind: nkParent)
case a.kind
of nkParent:
a.children = @[]
else: discard
I guess it makes sense as the kind could change at runtime so the compiler needs a case or if to infer it.
Still on a more complex code I have it is not working as expected even using case. I'll try to reproduce it on a minimal scenario
the problem seems to happen inside loops:
let list = @[Node(kind: nkParent), Node(kind: nkChild)]
for node in list:
case node.kind
of nkParent:
node.children = @[] # inside the loop results in warning, if not in a loop is fine as in previous post
of nkChild:
node.name = "foo" # same here
This is unfortunate but happens because you are initializing a ref object which allows unsafe partial initialization, making a NodeObj first errors the way it should without adding any warning flags:
type
NodeKind = enum
nkParent,
nkChild
Node = ref NodeObj
NodeObj {.acyclic.} = object
case kind: NodeKind
of nkParent:
children: seq[Node]
of nkChild:
name: string
let a = Node(NodeObj(kind: nkParent, name: ""))
error:
test.nim(14, 21) Error: a case selecting discriminator 'kind' with value 'nkParent' appears in the object construction, but the field(s) 'name' are in conflict with this value.
not sure if I got the issue right. If the problem is related to partial initialization, why it affects also accessors?
e.g.
# node is correctly initialized with all fields
let n = Node(kind: nkChild, name: "foo")
case n.kind
of nkChild: echo $n.name #using case statement, this access to name is fine and does not rise a warning
else: discard
# here all the nodes in the list are also fully initialized
let list = @[Node(kind: nkParent, children: @[]), Node(kind: nkChild, name: "hello")]
for node in list:
case node.kind
of nkChild: echo $node.name # here this time there is a warning
else: discard
The only difference between the 2 is the loop as far as I can see. Is the problem that the list is a list of references? Isn't also let Node(... creating a reference in the exact same way?
I do not see a warning when compiled with:
nim c -r --forceBuild --verbosity\:0 --hint\[Processing\]\:off --excessiveStackTrace\:on /tmp/test.nim
I also tried it with the --warning:ProveField:on flag, still no warning.
My nim version:
Nim Compiler Version 1.7.1 [MacOSX: amd64]
Compiled at 2022-07-19
Copyright (c) 2006-2022 by Andreas Rumpf
git hash: efcb89fa702da5bd5d2cf000ace759df90152895
active boot switches: -d:release
I'm on version 1.6.8 I can try updating
anyway if I run nim c -r --forceBuild --verbosity\:0 --hint\[Processing\]\:off --excessiveStackTrace\:on test.nim
or even just nim c test.nim
it compiles without warning and outputs
foo
hello
if I run with nim c --warning:ProveField:on test.nim though it outputs:
(23, 25) Warning: cannot prove that field 'node.name' is accessible [ProveField]
full source code:
type
NodeKind = enum
nkParent,
nkChild
Node = ref NodeObj
NodeObj {.acyclic.} = object
case kind: NodeKind
of nkParent:
children: seq[Node]
of nkChild:
name: string
let n = Node(kind: nkChild, name: "foo")
case n.kind
of nkChild: echo $n.name
else: discard
let list = @[Node(kind: nkParent, children: @[]), Node(kind: nkChild, name: "hello")]
for node in list:
case node.kind
of nkChild: echo $node.name # line 23
else: discard
I am able to reproduce with:
nim c -r --warning:ProveField:on /tmp/test.nim
Yes that is odd. I'm not sure why the warning triggers in the loop but not above. FWIW I can get the warning to go away with:
for node in list:
let n = node[]
case n.kind
of nkChild:
echo $n.name
else: discard