(I may have made some mistakes in this write-up, but I hope I have made the contradiction obvious by the time you get to the end. Please read to the end, because I may be identifying multiple problems, so if you feel I am wrong about one issue you should still consider the rest of this write-up.)
Let's start here:
Dotted keys define everything to the left of each dot as a table. Since tables cannot be defined more than once, redefining such tables using a [table]
header is not allowed. Likewise, using dotted keys to redefine tables already defined in [table]
form is not allowed.
The contradiction is here:
- Dotted keys define everything to the left of each dot as a table.
- tables cannot be defined more than once
Here, "define" must mean explicitly defined (because tables can be implicitly defined multiple times by either dotted keys or headers) and no delineation is made between tables "defined in dotted keys" versus tables defined in headers (referred to elsewhere in the spec as "directly defined"). It is stated that any table (explicitly) "defined" cannot be (explicitly) "defined" again. That means this should be invalid:
[fruit]
apple.color = "red" # Here, apple is "defined as a table"
apple.taste.sweet = true # Here apple is "defined" AGAIN as a table
I would also add that the spec should explicitly state that "dotted keys" can NOT refer to keys separated by dots inside headers. Otherwise, that would also mean that defining headers out of order would be invalid.
[fruit.apple]
# `fruit` is implicitly defined as a table
# `apple` is explicitly defined as a table within `fruit`
[fruit]
# fruit is explicitly defined as a table
# of course, this document should be valid!
Now, if you interpret "define" as implicitly defined in this case, that still doesn't work. More on this later.
We see the following elsewhere in the spec:
As long as a key hasn't been directly defined, you may still write to it and to names within it.
# This makes the key "fruit" into a table.
fruit.apple.smooth = true
# So then you can add to the table "fruit" like so:
fruit.orange = 2
The above is the only place in the spec where the term "directly defined" is used to delineate between the different kinds of "define". (Aside from where it says that in-line tables are "fully defined" at their location and cannot be added to or modified, but that is pretty air-tight so I don't want to focus on it at all.)
The (first) paragraph in question is followed by "The [table] form can, however, be used to define sub-tables within tables defined via dotted keys." I don't appreciate how the first two examples follow naturally from the principle "Since tables cannot be defined more than once" but the third example is a contradiction of the principle. It's fine for all three of these examples to be fine in TOML, but not without delineating between the different kinds of "define".
The contradiction leads me to question whether the following is an example of a valid contradiction of the aforementioned principle:
Is it valid for "dotted keys" (not in headers) to modify something implicitly defined as a table in a header? My guess is yes, since if you change the order it is valid.
[fruit.apple.taste]
# `fruit` is implicitly defined as a table
# `apple` is implicitly defined as a table in `fruit`
# `taste` is explicitly defined as a table in `apple`
[fruit]
# fruit is explicitly defined as a table
apple.color = "red"
# apple is (indirectly) defined as a table with color set to "red"
While there are occurrences in the spec where you can easily infer the difference between the kinds of "define" being used (including in some examples above), the top quotation seems to imply it is possible to possible to implement the "dotted keys" by only delineating between defined tables and implicitly defined tables (leaving aside arrays and in-line tables for now), when in fact, it is not possible. You HAVE TO delineate between dotted key definitions (not in headers) versus header definitions. There is no way to satisfy all the rules any other way.
Here is an example of an implementation. It's a bit TypeScripty, hopefully others can understand what I am getting at.
Define enum ScopeType
as having members: { Inferred, Defined }
Define scopeTypes
as a Map/Hash of Table -> ScopeType
.
Let global
be the global scope
A Map
pairs keys with values, and has methods "has", "get" and "set". "has" returns whether a mapping exists for a given key. "get" returns what a given key maps to. "set" maps a given key to a given value. In this case, ScopeTypes.has(value) is another way of expressing that a given value is not a literally defined value, e.g.1
or false
[a]
# assert global["a"] == undefined or scopeTypes.get(global["a"]) == ScopeType::Inferred
# define `a` as global["a"] or an empty table {}
# scopeTypes.set(a, ScopeType::Defined)
b.c = 1
# assert a["b"] == undefined or scopeTypes.has(a["b"])
# scopeTypes.set(b, ScopeType::Inferred)
[a.b] # should be forbidden
# assert global["a"] == undefined or scopeTypes.has(global["a"])
# define `a` as global["a"] or an empty table {}
# scopeTypes.set(a, ScopeType::Inferred)
# assert a["b"] == undefined or scopeTypes.get(a["b"]) == ScopeType::Inferred
# define `b` as a["b"] or an empty table {}
# scopeTypes.set(b, ScopeType::Defined)
The crux of the problem here is in our [a.b]
logic, where b
may only be Inferred
, in order to allow these cases:
[a.b.c]
[a.b]
# `a` is Inferred
# `b` was Inferred, but is now Defined
[a]
[a.b] # `a` is Defined
# `b` was undefined, is now Defined
And block this case:
[a.b]
# b is defined
[a.b]
# b is defined already! error!
Hence, there is no way to block "redefining such tables [tables defined by dotted keys] using a [table] header is not allowed." E.g.
[a]
b.c = 1
[a.b] # forbidden?
# `a` is Inferred
# b is must be Defined in order to block this
So let's rewrite our logic where we set b
in b.c = 1
to be Defined in order to account for the above case:
a.b = 1
# assert global["a"] == undefined or scopeTypes.get(global["a"]) == Defined
# define `a` as global["a"] or empty table {}
# scopeTypes.set(a, ScopeType::Defined)
# set a["b"] = 1
Now we can't block this:
[fruit.apple]
# fruit is Inferred
# apple is Defined
[fruit]
# fruit is Defined
apple.color = "red" # This is acceptable by the above logic
# To block this, we'd need to assert `apple` is not Defined
There is no way to block the above document without blocking the use of the same dotted-key-defined-table using only 2 ScopeType
enums.
a.b = 1
# `a` is Defined here
a.c = 2
# We must allow `a` to be Defined for this
# But if we allow `a` to be Defined, we can't block the previous case!
I am trying to get the point across using code here because I don't know of a better way to express this technically. We need to delineate between dotted key define's versus header define's in the spec in order for it to make logical sense.
(I may have made some mistakes in this write-up, but I hope I have made the contradiction obvious by the time you've managed to read this far.)
clarification