Skip to content

Fix #2338: error with mixed internal/external qualifiers in function declaration#3468

Open
zephyr111 wants to merge 1 commit intoispc:mainfrom
zephyr111:fix-2338-mixed-internal-and-external-qualifiers
Open

Fix #2338: error with mixed internal/external qualifiers in function declaration#3468
zephyr111 wants to merge 1 commit intoispc:mainfrom
zephyr111:fix-2338-mixed-internal-and-external-qualifiers

Conversation

@zephyr111
Copy link
Copy Markdown
Collaborator

Description

This PR fixes #2338 . An errors is reported when internal and external qualifiers are mixed in the same function declaration (not just a warning since it make no sense and the generated code only consider only one of the two keyword which is pretty bug-prone).

Related Issue

  • Linked to relevant issue(s)

Checklist

  • Code has been formatted with clang-format (e.g., clang-format -i src/ispc.cpp)
  • Git history has been squashed to meaningful commits (one commit per logical change)
  • Compiler changes are covered by lit tests
  • Language/stdlib changes include new functional tests for runtime behavior
  • Documentation updated if needed

@zephyr111 zephyr111 force-pushed the fix-2338-mixed-internal-and-external-qualifiers branch from 36fc668 to 6f3c883 Compare July 5, 2025 10:50
Copy link
Copy Markdown
Collaborator

@nurmukhametov nurmukhametov left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit picking comment about the commit message is below.

I generally prefer that the first line of the commit message should be as short as possible (ideally less than 50 characters), i.e., this is kind of the title of the commit message. Then starting from the 3rd line it may contain whatever text wrapped to something like 72 or 80.

I would do something like this for this commit:

Error for misused internal/external qualifiers

Report during function declaration construction the error about mixed 
internal/external qualifiers. Fix issue #2338

Comment thread src/module.cpp
if (isInternal && isExternal) {
const char *internalQualifier = isInline ? "inline" : "static";
const char *externalQualifier = isExternCorSYCL ? "extern" : "export";
Error(pos, "Function qualifier '%s' incompatible with '%s'.", externalQualifier, internalQualifier);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's just report the warning here. The initial request was about a warning, and I think it is better not to break users' builds on this occasion.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this is a bug like declaring a variable signed unsigned (which does not work in C/C++ nor in ISPC). Moreover, it is similar to static extern "C" which is already reported as an error in ISPC (see test5).
I do not see any use-case where it would be legit to mix such specifiers on the same function.
I am not strongly against reporting a warning though.

Please note export and extern "C" take precedence btw.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this is a bug like declaring a variable signed unsigned (which does not work in C/C++ nor in ISPC). Moreover, it is similar to static extern "C" which is already reported as an error in ISPC (see test5). I do not see any use-case where it would be legit to mix such specifiers on the same function. I am not strongly against reporting a warning though.

signed and unsigned contradict each other. Whereas we have inline that has a slightly unclear meaning (especially in different languages). It looks like C, C++ and ISPC all have different meanings for this function qualifier. According to docs, inline in ISPC is only about the requirement to always inline the function on its call sites. I don't see that it mentions anything about internal linkage of this function.

It is kind of surprise for me because I slightly doubt that it happens always, and I expected that inline implies internal linkage (similar to C). Knowing that, the original issue seems invalid to me, or it is actually about something different, e.g., inline keyword doesn't force inlining for some functions.

See an example here where inline and export mixed together, and they both work as expected: https://godbolt.org/z/EnqGvPP8n

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue with exported functions in ISPC is that they generate two versions of the function:

  1. An externally callable version (for use by the application).
  2. An internal version (for use within ISPC code).

When you annotate an exported function with inline, the attribute applies only to the internal version, not the externally visible one. This can be misleading, as users often assume the exported function exists only in one form (the external one).

A similar nuance exists with the unmasked keyword: ISPC emits a warning like
"unmasked qualifier is redundant for exported functions" - which is partially correct, since the qualifier has no effect on the external version, but still valid for the internal version.

From my experience, many users are unaware that export creates two versions of the function and often believe it only creates the external one.

So in ISPC, inline is simply a hint to the optimizer, instructing it to try inlining the function. While technically valid, using inline on exported or extern functions may cause confusion. It would be helpful to emit a warning in this case to clarify what’s actually happening. Something like: "'inline' on an 'export' function applies only to the internal ISPC version; the exported version is always emitted and may not be inlined."

On the other hand, using static with either export or extern is incorrect. static specifies internal linkage, which contradicts the external visibility intent of export and extern. In such cases, ISPC should emit a error.

Copy link
Copy Markdown
Collaborator Author

@zephyr111 zephyr111 Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the thorough explanation!
I am now convinced to use a warning with inline export (and inline extern "[...]").

By the way, regarding inline in C and C++, they do not exactly behave the same way (AFAIR static inline does and I think it should also be similar to static inline in ISPC or even just inline since AFAIK ISPC functions are implicitly static, hence the export and extern "C" keyword). In C, inline without extern is identical to static inline (i.e. internal linkage by default). In C++, inline without extern is not (i.e. external linkage by default). The way they handle external linkage of inlined function is also slightly different (so AFAIK C++ implementations should uses weak symbols while C implementations may not).

Technically, ISPC should have the same issue than C and C++ when an inline function is defined in a header and this function cannot be inlined (too big? recursive function?). The inlined function will be replicated in each TU causing link issues unless it is considered to be the exact same function (i.e. identical definition). I personally expect ISPC to behave like C in this case but I did not test it so far. I actually thought functions to be implicitly static, but it looks like this is not true (using static impact the result in Godbolt). I often forget there is two versions generated when extern is used in ISPC.

I guess we should emit an error in this case too (the resulting code will not link anyway -- see on Godbolt):

__attribute__((external_only))
inline export uniform int foo() { return 42; }

uniform int boo() { return foo(); }

Surprisingly, It looks like inlining is independent of whether there is an internal non-exported version of the function (see on Godbolt:

inline extern "C" uniform int foo() { return 42; }

uniform int boo() { return foo(); }

Even more interestingly, the generated code for this one looks wrong (see on Godbolt):

inline extern uniform int foo() { return 42; }

uniform int boo() { return foo(); }

This is supposed to be actually a valid code? This is typically the tricky case mentioned above where foo could be replicated in multiple TU (possibly with weak symbols). Here, it looks like ISPC ignore the extern so it assume extern inline is identical to inline, and also to static inline (i.e. internal linkage). Please note that foo can be put in a separate header file (i.e. isph) shared by two TU (i.e. two ispc files). Actually, the second ispc file could even do that (causing no error nor any warning so far -- see on Godbolt):

inline extern uniform int foo();

uniform int boo() { return foo(); }

The two last use-cases are a bit confusing to me, especially the last one.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... since AFAIK ISPC functions are implicitly static ...

No, they are not. It should be legit to have two ISPC translation units like these:

// tu1.ispc
int foo();
int boo() { return foo(); }
// tu2.ispc
int foo() { return 42; }

I actually thought functions to be implicitly static, but it looks like this is not true (using static impact the result in Godbolt).

Yes, they are not implicitly static.

I guess we should emit an error in this case too (the resulting code will not link anyway -- see on Godbolt):

__attribute__((external_only))
inline export uniform int foo() { return 42; }

uniform int boo() { return foo(); }

This code is incorrect even without inline and the linker reports error about it.

Surprisingly, It looks like inlining is independent of whether there is an internal non-exported version of the function (see on Godbolt:

inline extern "C" uniform int foo() { return 42; }

uniform int boo() { return foo(); }

Even more interestingly, the generated code for this one looks wrong (see on Godbolt):

inline extern uniform int foo() { return 42; }

uniform int boo() { return foo(); }

This is supposed to be actually a valid code?

I don't know. I am also not sure that these qualifiers were intended to be used with function definitions at all. I guess their primary goal was to be used with function declarations: extern for ISPC functions from a different ISPC TU, extern "C" for C functions from a different C TU.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even more interestingly, the generated code for this one looks wrong (see on Godbolt):

inline extern uniform int foo() { return 42; }

uniform int boo() { return foo(); }

The extern semantic in ISPC is not defined well. There is some description of it in the table at the end of https://ispc.github.io/ispc.html#interoperability-overview section which partially justifies the behavior. Note that using extern "C" keeps foo function alive.
https://godbolt.org/z/vfj6P69Tv

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

No warning when adding inline to export functions

3 participants