-
-
Notifications
You must be signed in to change notification settings - Fork 1.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Generics "sandwiched" between two modules don't mixin their scope symbols properly #11225
Comments
I'm not sure I'm buying that there is a bug here. When you write import
generic_library, helper_module
proc mixedIn: int = 100
proc makeUseOfLibrary*[T](x: T) =
libraryFunc(x) you're clearly aware that Notice how the casual reader can easily believe that Or to put it differently: The |
Never mind, there is a way to do this and keeping the "deadProc is never used" analysis. |
The real-world manifestations of this issue are much more WTF-ivoking than this simplified test case. I can point to more examples, but the two linked issues here are a good start. To clarify the expected behavior, I think the compiler should determine the lexical scope at the position where |
Yes, I read it and I do believe you. But it's a tradeoff here, just imagine the compiler could never reliably warn about unused symbols in e.g. |
How about this solution: # declare module-wise that these are mixins and also export them
mixin mixedIn*, indirectlyMixedIn*
proc libraryFunc*[T](x: T) =
echo indirectlyMixedIn() It would be a language change, but #7385 also indicates something like that is required. Of course, in today's Nim this can be written as: proc mixedIn*() = discard "overload one"
proc mixedIn*(x: typeof(nil)) = discard "overload two to make it a mixin" |
I think top level mixins are necessary in general - both because it's typical to use the same mixins in multiple procs and also because at the moment there is no way to refer to mixed in symbols in the right-hand side of a type section: type
Foo[T] = object
hashValue: type(hash(default(T)) I've encountered the need to use something like this on few occasions. But the other part of the proposal makes me feel uneasy. My suggestion tries to honour the standard behavior of lexical scopes. The forwarding approach seems less sound due to the following reasons:
|
Well
Well generics with their two different interacting scopes are simply harder to write that non-generic code. However, your point is valid. proc mixedIn: int = 100
proc makeUseOfLibrary*[T](x: T) =
libraryFunc(x) This is what it comes down to and maybe it shouldn't compile at all, regardless of the context? What is really done here is a form of implicit parameter passing |
As far as I understand, your concern about the proposed behavior is that it will interfere with the ability of the user or the compiler to reason about unused symbols. I think the analysis for the compiler is certainly possible. Every time a symbol gets mixed in at a call-site, this counts as an usage. Symbols with zero usages are still detected normally. The problem for the user is perhaps a bit overstated. Why would anyone bother to manually hunt for unused symbols if the compiler provides accurate hints for them? If I am confused and I want to know where a particular symbol is used, I can still invoke the "find references" operation in my IDE and |
This isn't a question about tooling, I do know how nimsuggest can figure it out. Here is another solution: Enforce that mixin symbols are exported then the code becomes proc mixedIn*: int = 100
proc makeUseOfLibrary*[T](x: T) =
libraryFunc(x) and everything is fine. IMO. |
This gets more tricky in the presence of other modules and re-exports. In the current example, I must also re-export Are you arguing that the current look-up rules are better or is this just some kind of "worse is better" argument, trying to avoid sinking time into implementing a fix? |
I'm arguing that the current lookup rules have their merits, otherwise too much magic would be going on for my taste.
Not really, but kind of yes, because I consider Not attaching procs to types has numerous downsides, most of them were unknown to me when I designed Nim and that's what we should focus our attention on. Currently not even the generic caching that we do is sound, remember? |
I really don't understand what part of the proposed solution can be described as "too much magic". All I am saying is that the compiler should follow the standard lexical scope rules - the files and their textual structure determine what is visible at any given line. You can reason about these rules by examining only the local file and its imports and the influence of modules that import you is limited to explicitly visible Concepts don't really change the rule of the game that much when it comes to mixed in symbols. One way to look at them is that they introduce a group of implicitly mixed in symbols. We need to fix the caching mechanics as a separate effort regardless of how the current issue is fixed. |
Today, after studying the compiler for a while, @mratsim was tripped by the same issue, but with a slight twist - this time the problem manifested as a run-time failure because a wrong overload was selected (see the referenced issue and fix above). |
Ok, so what to do:
|
@zah I discussed this problem with @Araq this morning. I do now understand the problem. But there was something in your example that bothered me, it is in this part: proc libraryFunc*[T](x: T) =
mixin mixedIn, indirectlyMixedIn
echo mixedIn()
echo indirectlyMixedIn() What is bothering me here is the part that you have a generic parameter, but then you don't use it at all. It has nothing to do with the generic instantiation of the procedure. So I changed the problem a bit to this: proc libraryFunc*[T](x: T) =
mixin mixedIn, indirectlyMixedIn
echo mixedIn(x)
echo indirectlyMixedIn(x) The parameter x is now used and the mixins are actually dependent of My second question, is T really a string type in your real word example, or did your problem simplification reduce it to string but in your real program it is actually a custom type. I am asking, because for the solution that I have in my head, it actually matters if the type is declared in your module, or if the type is declared in the standard library. I am currently exploring if argument dependent namespace lookup, a feature from c++ would be a good solution to this problem or not. Here is the slightly changed problem mapped to c++. // ----------------------------------------
// genericlibrary.hpp
#include <cstdio>
// Note that in c++ mixedIn cannot be resolved for types T that are not in namespace.
// Also note that mixedIn isn't forward declared here at all. The compile will later resolve it to mylib::mixedIn, because MyType is also in the namespace mylib
template <typename T>
auto libraryFunc(T x) -> void {
std::printf("mixed in: %d\n", mixedIn(x));
std::printf("mixed in: %d\n", indirectlyMixedIn(x));
}
// ----------------------------------------
// helpermodule.hpp
// ----------------------------------------
// moduleusinggenericlibrary.hpp
// #include "genericlibrary.hpp""
// #include "helpermodule.hpp"
template <typename T>
auto makeUseOfLibrary(T x) -> void {
libraryFunc(x);
}
// ----------------------------------------
// main.cpp
// #include "moduleusinggenericlibrary.hpp"
namespace mylib {
struct MyType {
int a,b;
};
auto mixedIn(MyType arg) -> int {
return 100;
}
auto indirectlyMixedIn(MyType arg) -> int {
return 200;
}
}
auto main() -> int {
mylib::MyType mt;
makeUseOfLibrary(mt);
return 0;
} Mapped to Nim, this would mean that |
@krux02, my test case was artificially simplified in order to produce the most minimal reproduction. If you want to look at some real-world examples, the linked issues above could help. Here is another one. You are right that there are some ADL-like properties in the real-world examples, but the picture is more complicated for the serialization library case, because there you must also support non-intrusive specialization for certain types. You probably remember the recent discussion about this in another RFC. Also, the ADL is not as clear-cut as you would hope - the "argument" may appear in any position in the call. |
@zah, I know that this issue is related to the But I am also not too happy with ADL. It does improve the situation for now, but I can also see that it is just a matter of time until we have extension modules and other complications in the language to address the shortcomings of ADL. |
As a first step to mitigate the problem, |
- includes type system workaround: generic sandwich nim-lang/Nim#11225 - converting NimNode to typedesc: nim-lang/Nim#6785
* Implement a Sage codegenerator for frobenius constants * Sage codegen for pairings * Autogen of endomorphism acceleration constants * The autogen fixed a copy-paste bug in lattice decomposition. We can use conditional negation now and save an add+dbl in scalar mul * small fixes * sage code for square root bls12-377 is not old * readme updates * Provide test suggestions for derive_frobenius * indentation + add equation form to sage * Sage test vector generator * Use the json vectors - includes type system workaround: generic sandwich nim-lang/Nim#11225 - converting NimNode to typedesc: nim-lang/Nim#6785 * Delete old sage code * Install nim-serialization and nim-json-serialization in CI * CI nimble install force yes
…lang#17255) * fixes nim-lang#11225; generic sandwich problems; [backport:1.2] * progress * delegating these symbols must be done via 'bind'
…lang#17255) * fixes nim-lang#11225; generic sandwich problems; [backport:1.2] * progress * delegating these symbols must be done via 'bind'
* feat(bench): PoC of integration with zkalc * feat(bench): zkalc prepare for adding pairing benches - generic type resulution issue * feat(bench): add pairing and G2 bench for zkalc and nimble build script * fix nimble dependencies * feat(bench-zkalc): polish and output json bench file * fix(nimble): version gate task-level dependencies * fix(deps): std/os instead of commandline for 1.6.x and gmp@#head * fix(deps): .nimble and nimble have different versioning format * fix(zkalc): workaround generic sandwich bug in Nim 1.6.x nim-lang/Nim#8677 nim-lang/Nim#11225 * fix(zkalc CI): skip zkalc build on 32-bit CI as it needs clang multilib * fix(zkalc CI): skip Windows as Clang throws fatal error LNK1107
This problem is a bit tricky to understand, so please read carefully.
Let's imagine we have a library featuring generic functions that mixes in some symbols from the caller scope:
generic_library.nim
This library is used from another module that provides the needed symbols.
mixedIn
is defined locally, whileindirectlyMixedIn
is imported from yet another module:module_using_generic_library.nim
helper_module.nim
So far, so good. If we compile
module_using_generic_library
, it will produce the expected output.But let's introduce another module that imports and uses
module_using_generic_library
:main.nim
If we try to compile it, we'll get the following error:
Why did this happen?
Please notice that the
makeUseOfLibrary
proc in themodule_using_generic_library
module is also generic. It is instantiated inmain
, but ultimately it consumes another generic defined ingeneric_library
. Its scope is "sandwiched" between themain
scope and the inner-mostgeneric_library
scope. At the momentmixin
fails to recognise this and attempts to look for symbols in the outer-most scope (the one ofmain
). This is highly surprising behavior for the user, because if you examine the lexical scopes,libraryFunc
seems instantiated properly in the sandwiched module.We've hit this problem multiple times during the development of nimcrypto, chronicles and nim-serialization. I've been asked to explain it on multiple occasions by almost everyone on our team, so it's an ongoing source of confusion and wasted time even for Nim experts. For this reason, I'm assigning high priority to the issue.
The text was updated successfully, but these errors were encountered: