Skip to content

Commit 6dc3da3

Browse files
authored
Upgrade to C++20 modules (#43)
2 parents d117de5 + c7d7d6b commit 6dc3da3

518 files changed

Lines changed: 8069 additions & 6633 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/premerge.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,11 @@ jobs:
4949
with:
5050
create-symlink: true
5151

52+
- name: Upgrade CMake
53+
run: |
54+
sudo apt-get purge cmake
55+
sudo snap install cmake --classic
56+
5257
- name: Configure CMake
5358
run: cmake --preset ${{env.CMAKE_PRESET}}
5459

CLAUDE.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ This is an experimental Python 3.9-compatible interpreter implementation in C++.
99
## Build System
1010

1111
### Prerequisites
12-
- CMake 3.25+
13-
- C++23 compiler
12+
- CMake 3.30+ (`CMAKE_EXPERIMENTAL_CXX_IMPORT_STD`, used for `import std`)
13+
- A C++26 compiler supporting C++20 named modules and `import std`
14+
(built and tested with GCC 16)
1415
- LLVM 23+ with MLIR (required for MLIR backend)
1516
- GMP (GNU Multiple Precision library)
1617
- ICU (International Components for Unicode)

CMakeLists.txt

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
cmake_minimum_required(VERSION 3.25)
1+
cmake_minimum_required(VERSION 3.30)
22
include(FetchContent)
33
include(ExternalProject)
44
include(CheckCXXSourceCompiles)
55

6+
set(CMAKE_EXPERIMENTAL_CXX_IMPORT_STD "f35a9ac6-8463-4d38-8eec-5d6008153e7d")
7+
68
project(python++)
79

810
set(CMAKE_CXX_STANDARD 26)
11+
set(CMAKE_CXX_SCAN_FOR_MODULES OFF)
12+
set(CMAKE_CXX_STANDARD_REQUIRED ON)
913

1014
include(cmake/CPM.cmake)
1115

12-
CPMAddPackage("gh:gabime/[email protected]")
16+
# spdlog 1.11+ can format through std::format instead of its bundled fmt.
17+
# SPDLOG_USE_STD_FORMAT keeps fmt out of the build entirely, which matters for
18+
# modules: spdlog/fmt/fmt.h drags 231 libstdc++ headers, and anything reaching a
19+
# module's global module fragment lands in its BMI and then collides with the
20+
# same headers #included by consumers.
21+
CPMAddPackage(
22+
NAME spdlog
23+
GITHUB_REPOSITORY gabime/spdlog
24+
VERSION 1.15.3
25+
OPTIONS "SPDLOG_USE_STD_FORMAT ON" "SPDLOG_BUILD_PIC ON"
26+
)
1327
CPMAddPackage("gh:google/[email protected]")
1428
CPMAddPackage("gh:jarro2783/[email protected]")
1529
CPMAddPackage("gh:Tessil/[email protected]")

cmake/PythonCppFlags.cmake

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
# Helper for giving every first-party target the same compiler flags.
1+
# Helper for giving every first-party target the same compiler flags and the
2+
# same C++ module configuration.
23
#
34
# The flags come from the external `project_options` package (added with CPM in
45
# the top-level CMakeLists.txt), which exposes them as two INTERFACE targets:
@@ -16,6 +17,19 @@
1617
# not the usage requirements of libraries linked afterwards. Linking the flags
1718
# to `<name>` alone would therefore silently compile nothing with them, so this
1819
# always covers the `obj.<name>` twin as well.
20+
#
21+
# The same `obj.<name>` split applies to C++ module settings, and there it is
22+
# easier to miss: `CXX_SCAN_FOR_MODULES` set on `<name>` does not reach the
23+
# object library that actually compiles the sources, so those sources are built
24+
# by CMake's "unscanned" rule with no `-fmodule-mapper`. A TU that imports
25+
# `py.runtime` then fails with either "'import' does not name a type" or, worse,
26+
# a fallback lookup in `gcm.cache/`. Setting the properties on both twins is
27+
# what makes `import py.runtime;` work inside the MLIR layer.
28+
#
29+
# Note: do NOT add `-fmodules` here. CMake supplies `-fmodules-ts` together with
30+
# `-fmodule-mapper=` on its scanned compile rules; adding the flag by hand also
31+
# applies it to unscanned targets, which turns a clear diagnostic into a
32+
# confusing module-not-found error.
1933

2034
include_guard(GLOBAL)
2135

@@ -28,8 +42,26 @@ function(python_cpp_link_project_options)
2842
get_target_property(type ${name} TYPE)
2943
if(type STREQUAL "INTERFACE_LIBRARY")
3044
target_link_libraries(${name} INTERFACE project_options project_warnings)
31-
else()
32-
target_link_libraries(${name} PRIVATE project_options project_warnings)
45+
continue()
46+
endif()
47+
48+
target_link_libraries(${name} PRIVATE project_options project_warnings)
49+
50+
# Everything first-party either provides or consumes `py.runtime`, so
51+
# scan it all. Scanning costs ~0.16s per TU (a preprocess-only pass) and
52+
# removes a whole class of "this target cannot see the module" failures.
53+
set_target_properties(${name} PROPERTIES CXX_SCAN_FOR_MODULES ON
54+
CXX_MODULE_STD ON)
55+
56+
# Module imports resolve through link dependencies, so every consumer
57+
# needs a path to python-runtime - the sole provider of `py.runtime`.
58+
# It is linked directly rather than via python-cpp because python-cpp and
59+
# python-mlir are mutually dependent: a module provider reached only
60+
# through a link cycle cannot be ordered before its consumers, and they
61+
# compile with an empty module map. python-runtime itself sits below that
62+
# cycle, so linking it here is always acyclic.
63+
if(TARGET python-runtime AND NOT ${target} STREQUAL "python-runtime")
64+
target_link_libraries(${name} PRIVATE python-runtime)
3365
endif()
3466
endforeach()
3567
endforeach()

integration/program.cpp

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
#include "executable/Program.hpp"
2-
#include "executable/bytecode/Bytecode.hpp"
3-
#include "interpreter/Interpreter.hpp"
4-
#include "parser/Parser.hpp"
5-
#include "runtime/PyDict.hpp"
6-
#include "runtime/PyFrame.hpp"
7-
#include "runtime/PyInteger.hpp"
8-
#include "runtime/PyList.hpp"
9-
#include "runtime/PyNumber.hpp"
10-
#include "runtime/PyObject.hpp"
11-
#include "runtime/PyString.hpp"
12-
#include "runtime/PyTuple.hpp"
13-
#include "runtime/types/builtin.hpp"
14-
#include "vm/VM.hpp"
1+
#include "core.hpp"
2+
#include "executable/common.hpp"
153

164
#include "gtest/gtest.h"
5+
#include <gmpxx.h>
6+
#include <spdlog/spdlog.h>
7+
8+
#include <cmath>
9+
10+
import py.ast;
11+
import py.types;
12+
import py.lexer;
13+
import py.runtime;
14+
import std;
1715

1816
using namespace py;
1917

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# An unset __slots__ entry must read as unset, however the slot storage was recycled.
2+
#
3+
# The storage lives in extra bytes past the object, and both the GC and the member
4+
# accessor test each entry against null. Slab memory is poisoned rather than zeroed,
5+
# so if the allocator does not clear those bytes an unset slot reads back as a
6+
# non-null garbage pointer: the accessor returns it instead of raising AttributeError,
7+
# and the GC dereferences it.
8+
9+
10+
class C:
11+
__slots__ = ("a", "b", "c")
12+
13+
14+
def unset_raises(obj, name):
15+
try:
16+
getattr(obj, name)
17+
except AttributeError:
18+
return True
19+
else:
20+
return False
21+
22+
23+
c = C()
24+
c.a = 1
25+
assert c.a == 1
26+
assert unset_raises(c, "b"), "unset slot 'b' should raise AttributeError"
27+
assert unset_raises(c, "c"), "unset slot 'c' should raise AttributeError"
28+
29+
# Churn so that later instances land on slots that were freed and poisoned.
30+
for i in range(2000):
31+
x = C()
32+
x.a = i
33+
x.b = i
34+
x.c = i
35+
36+
for i in range(2000):
37+
y = C()
38+
y.a = i
39+
assert y.a == i
40+
assert unset_raises(y, "b"), "recycled slot 'b' should still read as unset"
41+
assert unset_raises(y, "c"), "recycled slot 'c' should still read as unset"
42+
43+
# Slots that are set must survive a collection with their values intact. The list is a
44+
# heap object reachable only through the slot, so the GC has to trace the slot correctly.
45+
kept = []
46+
for i in range(500):
47+
z = C()
48+
z.a = i
49+
z.b = [i, i + 1]
50+
kept.append(z)
51+
52+
i = 0
53+
for z in kept:
54+
assert z.a == i
55+
assert z.b == [i, i + 1]
56+
assert unset_raises(z, "c")
57+
i += 1
58+
59+
print("slots_uninitialised: ok")

0 commit comments

Comments
 (0)