Skip to content

C++20 modules: support gnu emulation modes and extended floating-point types - #315

Open
soroshsabz wants to merge 6 commits into
edgcpp:mainfrom
soroshsabz:feature/gcc-module-support
Open

soroshsabz wants to merge 6 commits into
edgcpp:mainfrom
soroshsabz:feature/gcc-module-support

Conversation

@soroshsabz

@soroshsabz soroshsabz commented Oct 16, 2026 •

Copy link
Copy Markdown

ITNOA

Hi,

Note: This PR bundles two contributions: the modules engine fix (main,
described first) and Windows development-environment fixes (listed at the
end). They can be split into separate PRs on request.

Problem

Compiling any module interface unit in a gnu or clang emulation mode
(--gnu_version / --clang_v together with --modules) failed:

Catastrophic error: an IFC file could not be produced for the current
          translation unit
            one or more entities cannot currently be written to an IFC file

even for a trivial export int f();. This blocks module support for gcc
dialects entirely — including EDG-based IntelliSense for g++ projects
(microsoft/vscode-cpptools#6302).

Root cause

Writing a module interface unit serializes the declarations of the scopes it
touches, and the failure is a four-step chain (each step verified by a gdb
breakpoint on ifc_write_catastrophe in a debug build):

  1. Predeclaration. In emulation modes, the predefined type entry
    processing (enter_predefined_typedef in src/sys_predef.c, around
    line 3091) enters typedefs such as _Float16 — fk_std_float16 for
    --gnu_version >= 130000 — plus __float80, __float128, _Float32x,
    _Float64x, etc. into the global scope. These are compiler-generated
    declarations; user code did not mention them.

  2. Unconditional scope walk. When the IFC file is written,
    ifc_modules_write_out recursively dumps the scopes reachable from the
    module (dump_scope_recursively → dump_scope_types in
    src/ifc_modules_write.c), which enters every type in those scopes —
    including the predeclared typedefs from step 1, whether or not the module
    uses them. This is why even export int f(); failed: the walk reaches
    the predeclared typedefs regardless of the module's contents.

  3. Type entry. Entering the typedef enters its underlying type, landing
    in an_ifc_il_map::enter_float_type (src/ifc_modules_write.c:2691).

  4. Incomplete encoding table. That function's switch had IFC encodings
    for only four float kinds — fk_float, fk_double, fk_long_double,
    fk_float128. Every extended kind (fk_float16, fk_fp16,
    fk_std_float16, fk_std_float32/64/128, fk_float32x, fk_float64x,
    fk_float80, fk_std_bfloat16) called ifc_write_catastrophe(),
    terminating the compilation.

Evidence — the observed backtrace (frames elided between):

dump_scope_types            ifc_modules_write.c:6045
→ enter_typedef             ifc_modules_write.c:5890   ("_Float16")
→ find_or_enter_type / enter_type
→ enter_float_type          ifc_modules_write.c:2731   (fk_std_float16)
→ ifc_write_catastrophe

Why the existing tests never caught this: plain --c++20 mode predeclares
none of these typedefs into the walked scope, so the entire in-tree modules
suite (which runs only in default mode) was unaffected — and, before this PR,
contained no emulation-mode module test at all.

Why the fix is correct

Two complementary changes, one per root cause:

  • Skip predeclared typedefs in the scope walk (dump_scope_types):
    these declarations are generated by the compiler itself, and any importing
    translation unit runs the same predefined type entry processing with the
    same dialect options before reading the IFC file — recreating them. They
    are therefore not part of the module's interface and never need to be
    serialized. The skip only stops the unconditional walk of
    compiler-generated typedef declarations; types that are actually used
    by exported entities are still entered through the encoding of those
    entities.
  • Encode the interchange float kinds (enter_float_type writer +
    ifc_tbs_float reader): _Float16/__fp16/std::float16_t →
    float+bit16, _Float32 → bit32, _Float64 → bit64,
    _Float128/std::float128_t → bit128 (joining the existing
    __float128 mapping), so interfaces can legitimately export entities
    involving those types. The reader previously asserted on any precision
    other than default (even __float128 could not be read back); bit16
    now selects the _Float16 kind matching the current dialect, mirroring
    the predefined type entry processing.

Limitations (documented in the changelog)

_Float32x, _Float64x, __float80, and std::bfloat16_t still cannot be
written: the IFC fundamental-type encoding has no representation that
distinguishes them from other floating-point types of the same size. An
attempt to export an entity involving them still produces the same
catastrophic error.

Testing

  • New regression tests, passing on both day-posture configs
    (edg_x86_64, edg_x86_64_cp):
    • modules/eifc/modules/import-function-gnu/ (rp): gnu-mode interface
      compilation, import, link, and run.
    • modules/eifc/modules/export-float16-gnu/ (cp): _Float16 in an
      exported interface, round-tripping through the IFC file.
  • Full modules suite: observed failures identical to expectations (45/45
    lines, zero diffs). regressions/modules: unchanged.
  • Manual: with --c++20 --gnu=150100 --modules, interface units compile,
    _Float16-returning exports import and link, and the program runs.

Addresses #303 — whose
accepted answer confirms the general gap ("There are plenty of entity types
that we cannot currently write out to IFC files"); this PR closes the part
of that gap that blocked emulation modes entirely.


Windows development environment fixes (bundled)

Commits: Improve Windows dev, Add Windows environment bootstrap script, Keep pre-push interaction in the user terminal on Windows

Problem

A default Windows clone of the repository cannot build or test, and the
pre-push hook fails invisibly:

  1. Git for Windows defaults to core.autocrlf=true, producing a CRLF
    checkout. This breaks the build (mk_errinfo cannot parse
    src/error_msg.txt) and the Docker workflow (every shebang script in
    dev_tools/bin fails inside the container with
    env: 'python3\r': No such file or directory).
  2. edg-exec crashed with ValueError: Paths don't have the same drive
    whenever %APPDATA% and the checkout are on different drive letters.
  3. The pre-push hook (a) required PYTHONPATH to be configured even though
    git invokes it with the ambient environment, (b) relaunched itself in a
    new console window that closes before any output can be read, and
    (c) crashed with a bare TypeError when edg-exec was not on PATH.
  4. edg-exec misdetected the NUL device as a TTY on Windows (os.isatty
    returns TRUE for it), requesting -it for containers run with
    stdin=DEVNULL; docker's TTY attach then failed and — because the
    docker exit status was discarded — the failure was silently swallowed.

Changes

  • dev_tools/pylibs/edgdocker/__init__.py:
    • _check_parents: treat paths on different drives as non-overlapping.
    • _stdin_is_interactive_console(): detect a real console via
      GetConsoleMode instead of os.isatty (fixes the NUL
      misdetection).
    • Propagate the docker exit status on Windows (matching the os.execvp
      semantics of the Linux path).
  • .git-hooks/pre-push.py:
    • Locate dev_tools/pylibs relative to the script, so the hook works
      without PYTHONPATH configured.
    • Prompt in the user's current terminal using CONIN$ (the Windows
      counterpart of /dev/tty). The relaunch into a standalone console is
      kept as a fallback for environments with no attached console (GUI git
      clients, mintty), and that fallback now pauses before closing on
      failure so its output can be read.
  • dev-env.ps1 (new): PowerShell counterpart of .envrc; dot-source to
    configure PATH/PYTHONPATH for a session.
  • dev-init.py: on Windows, failure commentary now suggests . dev-env.ps1
    for the current session and a SetEnvironmentVariable one-liner for a
    permanent setup, before the existing env-dialog instructions.
  • HACKING.md: document dev-env.ps1 and warn about CRLF checkouts.

All changes are Windows-only paths; Linux behavior was verified unchanged
(import/compile checks in the dev container, os.isatty equivalence,
/dev/tty branch untouched).

@soroshsabz
soroshsabz force-pushed the feature/gcc-module-support branch from d20dcf3 to c23faa6 Compare October 16, 2026 15:26
@soroshsabz

Copy link
Copy Markdown
Author

@daveedvdv-nvidia Could you please review this PR?

@wchilders-nvidia wchilders-nvidia left a comment

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.

Looks very promising!

except ValueError:
# On Windows, paths on different drives have no common prefix (e.g.,
# APPDATA on "C:" and the workspace on "E:"), so they cannot overlap.
continue

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.

We probably want to do a bit more interrogation as this is a sort of safety check(?)

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good point. Replaced the blanket except ValueError with an explicit check in 3d96ddb: the overlap check is now skipped only when the two paths are on genuinely different drives (os.path.splitdrive comparison), documented as "neither path can contain the other". Same-drive paths always get the full commonpath check, and any other unexpected condition now raises loudly instead of being silently skipped.

Comment thread .git-hooks/pre-push.py
# "dev_tools/pylibs" relative to this script so "edgutil" can be imported.
sys.path.insert(
0, str(Path(__file__).resolve().parents[1] / 'dev_tools' / 'pylibs')
)

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.

I don't love this, I'd prefer people actual configure the environment (which dev-init.py should help with ... which also installs the script?). However, if we're only doing this for the pre-push.py script (as a sort of "balancing act" to handle weird edge cases), I think that's fine.

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Understood: it is deliberately only in pre-push.py, as the balancing act you describe. Every other tool is invoked from a configured environment, but git runs this hook with the ambient environment, and nothing surfaced that distinction to the user today (the failure was an ImportError inside a closing console window). And yes, dev-init.py installs the hook (it writes the .git/hooks/pre-push shim that executes .git-hooks/pre-push.py), so dev-init users get the self-locating version automatically.

Comment thread .git-hooks/pre-push.py
# in the user's current terminal.
try:
sys.stdin = open('CONIN$', 'r')
except OSError:

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.

Confession: I wrote the original logic here, I'm not a "Windows guy" but it seemed to work and be reasonably correct; I'll defer to others that know Windows better as to if this is an improvement.

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the context! The Windows rationale: CONIN$ is the console input device and works like /dev/tty: it can be opened regardless of what stdin is connected to (git feeds the hook the ref list on stdin, so stdin itself can't serve the prompts). With a console attached (cmd, PowerShell, Windows Terminal), everything now stays in the user's current terminal. Your relaunch is preserved as the fallback for the cases where CONIN$ cannot open (GUI git clients, mintty-style pseudo-terminals), so that intent is kept; that fallback window also now waits for Enter on failure so its output can't flash away.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

cmd and PowerShell are shells, and Windows Terminal is a terminal GUI. From the Windows perspective, none of them are consoles; only conhost.exe and openconsole.exe are.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For performance, NtQueryVolumeInformationFile should be used, but since this is a Python script, the current approach is acceptable.

Comment thread .git-hooks/pre-push.py Outdated
except SystemExit as exit_request:
if exit_request.code != 0:
pause_delegate_window()
raise

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.

This seems mostly reasonable, although, I think we should probably just handle this as:

returncode = checker.run()
if returncode != 0:
  pause_delegate_window()
sys.exit(returncode)

rather than doing the exception based mechanism.

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 3d96ddb, exactly along those lines: checker.run() now returns an exit status (int), and the mid-run sys.exit call sites (policy failure in check_commits, the q answer, and the too-many-attempts case in confirm_okay) were converted to an abort flag, so every failure path flows through main():

def main():
  checker = PrePushChecker()
  returncode = checker.run()
  if returncode != 0:
    pause_delegate_window()
  sys.exit(returncode)

Comment thread dev_tools/pylibs/edgdocker/__init__.py Outdated
console_mode = ctypes.c_uint(0)
return bool(
kernel32.GetConsoleMode(stdin_handle, ctypes.byref(console_mode))
)

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.

We used this same sort of check in a few different tools. It would probably be good to generalize this in edgutil as something like edgutil.is_interactive_terminal().

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 3d96ddb: the check now lives in edgutil as edgutil.is_interactive_terminal() (with the GetConsoleMode rationale in its docstring), and edgdocker calls it. Happy to switch other tools over to it in a follow-up if you point me at them.

Comment thread src/Changes
entity involving them to an IFC file is still rejected with the same
catastrophic error.


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.

We historically haven't done changes entries for "incomplete" features like modules; that's something we might want to reconsider.

I'll also note that typically we try to keep changes entries brief and high level, this is a bit too long and thorough for that guideline.

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Shortened in 8289ec9 to a single brief paragraph (bug and fix, no examples). On the policy question: I'd leaned on the entry being about a fix (modes were broken) rather than the incomplete feature itself, but if you'd rather keep module entries out of the changelog entirely until the coverage gap is closed, I'm happy to drop it; your call.

Comment thread src/ifc_modules_read.c Outdated
in sys_predef.c). */
result = float_type((gnu_version_is(>= 130000) ||
clang_version_is(>= 150000)) ?
fk_std_float16 : fk_float16);

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.

I'd personally be a fan of adding a small INLINE function helper (or macro) to types.h so this logic is centralized between sys_predef.c and modules.

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Done in 8289ec9: INLINE a_float_kind float16_kind(void) now lives in types.h (next to skip_typerefs), and both sys_predef.c and the IFC reader use it. (lang_feat.h, which defines the version macros, is already pulled in ahead of types.h through basic_hdrs.h in the standard include order, so the inline definition is safe.)

Comment thread src/ifc_modules_write.c
case fk_fp16:
case fk_std_float16:
set_ifc_basis(&fund_type, ifc_tbs_float);
set_ifc_precision(&fund_type, ifc_tps_bit16);

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.

I'm a little worried about treating fk_float16 and fk_fp16 the same as they have different manglings so if this shows up somewhere mangling-relevant we could end up in a weird place. It's probably better to move fp16 to the list of cases we're going to have to handle separately.

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Good catch, and it's exactly a round-trip hazard: the reader maps float+bit16 back to the _Float16 kind, so an exported __fp16 would have come back with a different mangling. Moved fk_fp16 to the still-rejected group in 8289ec9, with a comment noting the distinct mangling.

Comment thread dev-env.ps1
}

Remove-Variable -Name edgRepoRoot, edgToolDirs, edgPyLibs `
-ErrorAction SilentlyContinue

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.

Neat!

@soroshsabz soroshsabz Oct 16, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks! :)

This branch has not been deployed

No deployments
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.

3 participants