diff --git a/CLAUDE.md b/CLAUDE.md index bc029ee5..17f41a3b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -725,6 +725,27 @@ compiles each generated header after the root, checks that each one *fails* on i and runs the `ce/*.cpp` examples against the generated tree. Those examples are also ordinary tests in the CMake build (`ce/CMakeLists.txt`), so they cannot rot silently. +### Compiler Explorer links in the documentation + +**A link from a doc page uses the normal includes, not the flattened headers.** The example +includes `` as it would locally, and the link selects Boost from CE's +Libraries list. The flattened headers are for the `ce/` examples only. + +`dev/ce_link.py` generates the link from the tagged region the page includes, as a +`godbolt.org/clientstate/` URL that carries the source itself: + +```bash +dev/ce_link.py doc/modules/ROOT/examples/ast.cpp +``` + +Its constants fix the compiler (x86-64 clang 23.1.0), the options (`-std=c++17 -O2`) and the Boost +release (1.92). The NOTE at the top of `performance.adoc` states all three, so change them together. + +- **A link runs the Boost release CE offers, not this tree.** An example that uses something newer + than that release breaks its link. Compile it against that release's headers before linking it. +- **Nothing regenerates a link.** When an example changes, its link keeps the old code until + `dev/ce_link.py` is re-run and the new URL pasted into the page. + ### Custom RTTI When `` is unavailable or insufficient, use static_rtti or implement custom RTTI. See `doc/modules/ROOT/examples/custom_rtti/` and policies in `include/boost/openmethod/policies/`. diff --git a/dev/ce_link.py b/dev/ce_link.py index 3a5c224b..e80642f3 100755 --- a/dev/ce_link.py +++ b/dev/ce_link.py @@ -15,11 +15,16 @@ The source is the tagged region the page includes - `tag=content` by default - so the reader gets exactly the code shown above the link. -With `--beside`, the two examples are compiled side by side, each in its own -compiler pane, to compare the assembly: +With `--column EXAMPLE:TAG`, given once per column, the link opens the +examples side by side, each source above its assembly, to compare the code +generated for several functions. A column holds the tagged region of its +example, minus every region nested in it except `TAG`, so that its assembly +shows that one function: - dev/ce_link.py doc/modules/ROOT/examples/dispatch_open_method.cpp \\ - --beside doc/modules/ROOT/examples/dispatch_virtual_function.cpp + dev/ce_link.py \\ + --column doc/modules/ROOT/examples/dispatch_virtual_function.cpp:virtual_function \\ + --column doc/modules/ROOT/examples/dispatch_open_method.cpp:method_via_ref \\ + --column doc/modules/ROOT/examples/dispatch_open_method.cpp:method_via_virtual_ptr An example that includes a Boost header gets the Boost release that Compiler Explorer offers in its Libraries dropdown, not the headers of this tree. @@ -42,15 +47,16 @@ "demangle": True, "labels": True, # CE takes a function whose every instruction comes from a header for - # library code: `call_via_virtual_ptr` is the inlined dispatch, all of it. + # library code: `method_via_virtual_ptr` is the inlined dispatch, all + # of it. "libraryCode": False, "directives": True, "commentOnly": True, } -TAG_DIRECTIVE = re.compile(r"\s*// (tag|end)::\w+\[\]\s*$") +TAG_DIRECTIVE = re.compile(r"\s*// (tag|end)::(\w+)\[\]\s*$") -def tagged_region(path, tag): +def tagged_region(path, tag, only=None): begin, end = f"// tag::{tag}[]", f"// end::{tag}[]" lines = path.read_text().splitlines(keepends=True) starts = [i for i, line in enumerate(lines) if line.strip() == begin] @@ -60,9 +66,31 @@ def tagged_region(path, tag): sys.exit(f"{path}: expected exactly one `{begin}` ... `{end}`") # Like Asciidoctor, drop the directives of the tags nested in the region. - region = lines[starts[0] + 1 : ends[0]] + # With `only`, also drop the regions of the nested tags other than `only`. + kept, nested, skipping = [], set(), None - return "".join(line for line in region if not TAG_DIRECTIVE.match(line)) + for line in lines[starts[0] + 1 : ends[0]]: + directive = TAG_DIRECTIVE.match(line) + + if directive: + kind, name = directive.groups() + nested.add(name) + + if only and name != only: + skipping = name if kind == "tag" else None + elif not skipping: + kept.append(line) + + if only and only not in nested: + sys.exit(f"{path}: no `// tag::{only}[]` inside `{begin}`") + + source = "".join(kept) + + if only: + # A dropped region leaves the blank lines that surrounded it. + source = re.sub(r"\n{3,}", "\n\n", source).rstrip() + "\n" + + return source def uses_boost(source): @@ -98,7 +126,7 @@ def editor(id, source): return component("codeEditor", id=id, lang="c++", source=source) -def beside_state(base, source, compiler): +def columns_state(sources, compiler): def column(id, source): return { "type": "column", @@ -117,7 +145,9 @@ def column(id, source): ], } - return {"content": [{"type": "row", "content": [column(1, base), column(2, source)]}]} + columns = [column(id, source) for id, source in enumerate(sources, 1)] + + return {"content": [{"type": "row", "content": columns}]} def clientstate_url(state): @@ -133,21 +163,34 @@ def clientstate_url(state): def main(): parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) - parser.add_argument("example", type=Path) + parser.add_argument("example", type=Path, nargs="?") parser.add_argument("--tag", default="content") parser.add_argument( - "--beside", type=Path, help="compile this example beside, for the assembly" + "--column", + action="append", + metavar="EXAMPLE:TAG", + help="a column showing the function in the nested region TAG", ) - parser.add_argument("--beside-tag", help="the tag of the other example (default: --tag)") parser.add_argument("--compiler", default=COMPILER, help="CE compiler id") args = parser.parse_args() - source = tagged_region(args.example, args.tag) - if args.beside: - other = tagged_region(args.beside, args.beside_tag or args.tag) - state = beside_state(other, source, args.compiler) + if bool(args.example) == bool(args.column): + parser.error("give either an example or --column, not both") + + if args.column: + sources = [] + + for spec in args.column: + path, sep, only = spec.rpartition(":") + + if not sep or not path or not only: + parser.error(f"--column {spec}: expected EXAMPLE:TAG") + + sources.append(tagged_region(Path(path), args.tag, only)) + + state = columns_state(sources, args.compiler) else: - state = session_state(source, args.compiler) + state = session_state(tagged_region(args.example, args.tag), args.compiler) print(clientstate_url(state)) diff --git a/doc/modules/ROOT/examples/ast.cpp b/doc/modules/ROOT/examples/ast.cpp index 6912cbd7..e3ca92bc 100644 --- a/doc/modules/ROOT/examples/ast.cpp +++ b/doc/modules/ROOT/examples/ast.cpp @@ -85,10 +85,10 @@ int main() { } // end::content[] -void call_via_ref(const Node& node, std::ostream& os) { +void method_via_ref(const Node& node, std::ostream& os) { postfix(node, os); } -void call_via_virtual_ptr(virtual_ptr node, std::ostream& os) { +void method_via_virtual_ptr(virtual_ptr node, std::ostream& os) { postfix(node, os); } diff --git a/doc/modules/ROOT/examples/ast_virtual_function_2.cpp b/doc/modules/ROOT/examples/ast_virtual_function_2.cpp index c0872c46..4c0da188 100644 --- a/doc/modules/ROOT/examples/ast_virtual_function_2.cpp +++ b/doc/modules/ROOT/examples/ast_virtual_function_2.cpp @@ -51,6 +51,6 @@ int main() { } // end::all[] -void call_virtual_function(const Node& node, std::ostream& os) { +void virtual_function(const Node& node, std::ostream& os) { node.postfix(os); } diff --git a/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp b/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp index 6e3ada52..a5f60deb 100644 --- a/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp +++ b/doc/modules/ROOT/examples/custom_rtti/1/custom_rtti.cpp @@ -120,6 +120,6 @@ int main() { std::cout << " = " << e.value() << "\n"; // 2 3 + 4 * = 20 } -void call_via_ref(const Node& node, std::ostream& os) { +void method_via_ref(const Node& node, std::ostream& os) { postfix(node, os); } diff --git a/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp b/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp index 87fc9657..349a5d0c 100644 --- a/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp +++ b/doc/modules/ROOT/examples/custom_rtti/2/custom_rtti.cpp @@ -114,6 +114,6 @@ int main() { std::cout << value(e) << "\n"; // 2 3 + 4 * = 20 } -auto call_via_ref(const Node& node) { +auto method_via_ref(const Node& node) { return value(node); } diff --git a/doc/modules/ROOT/examples/dispatch_open_method.cpp b/doc/modules/ROOT/examples/dispatch_open_method.cpp index 7db59b11..31353ea8 100644 --- a/doc/modules/ROOT/examples/dispatch_open_method.cpp +++ b/doc/modules/ROOT/examples/dispatch_open_method.cpp @@ -18,17 +18,17 @@ using boost::openmethod::virtual_ptr; BOOST_OPENMETHOD(postfix, (virtual_ptr node, std::ostream& os), void); -// tag::call_via_ref[] -void call_via_ref(const Node& node, std::ostream& os) { +// tag::method_via_ref[] +void method_via_ref(const Node& node, std::ostream& os) { postfix(node, os); } -// end::call_via_ref[] +// end::method_via_ref[] -// tag::call_via_virtual_ptr[] -void call_via_virtual_ptr(virtual_ptr node, std::ostream& os) { +// tag::method_via_virtual_ptr[] +void method_via_virtual_ptr(virtual_ptr node, std::ostream& os) { postfix(node, os); } -// end::call_via_virtual_ptr[] +// end::method_via_virtual_ptr[] // end::content[] int main() { diff --git a/doc/modules/ROOT/examples/dispatch_virtual_function.cpp b/doc/modules/ROOT/examples/dispatch_virtual_function.cpp index 9ba1f6b1..0dcd9c01 100644 --- a/doc/modules/ROOT/examples/dispatch_virtual_function.cpp +++ b/doc/modules/ROOT/examples/dispatch_virtual_function.cpp @@ -14,11 +14,11 @@ struct Node { virtual void postfix(std::ostream& os) const = 0; }; -// tag::call_virtual_function[] -void call_virtual_function(const Node& node, std::ostream& os) { +// tag::virtual_function[] +void virtual_function(const Node& node, std::ostream& os) { node.postfix(os); } -// end::call_virtual_function[] +// end::virtual_function[] // end::content[] int main() { diff --git a/doc/modules/ROOT/pages/performance.adoc b/doc/modules/ROOT/pages/performance.adoc index 8d5dbe20..e8e73f78 100644 --- a/doc/modules/ROOT/pages/performance.adoc +++ b/doc/modules/ROOT/pages/performance.adoc @@ -14,7 +14,7 @@ function call: [source,c++] ---- -include::{examplesdir}/dispatch_virtual_function.cpp[tag=call_virtual_function] +include::{examplesdir}/dispatch_virtual_function.cpp[tag=virtual_function] ---- Clang compiles this function to the following assembly on the x64 architecture: @@ -31,7 +31,7 @@ Let's look at a method call now: [source,c++] ---- -include::{examplesdir}/dispatch_open_method.cpp[tag=call_via_ref] +include::{examplesdir}/dispatch_open_method.cpp[tag=method_via_ref] ---- This compiles to (variable names are shortened for readability): @@ -65,8 +65,8 @@ a single virtual argument _via_ _a_ _reference_ is between 30% and 50% slower than calling the equivalent virtual function, with an empty body and no other arguments. In most real programs, the overhead would be unnoticeable. -*However*, `call_via_ref` does two things: it constructs a `virtual_ptr` -from a `const Node&`, then it calls the method. +*However*, `method_via_ref` does two things: it constructs a +`virtual_ptr` from a `const Node&`, then it calls the method. The construction of the `virtual_ptr` is the costly part. It performs a lookup in a perfect hash table, indexed by pointers to `std::type_info`, to find the @@ -85,7 +85,7 @@ If we already have a `virtual_ptr`: [source,c++] ---- -include::{examplesdir}/dispatch_open_method.cpp[tag=call_via_virtual_ptr] +include::{examplesdir}/dispatch_open_method.cpp[tag=method_via_virtual_ptr] ---- A method call compiles to: @@ -96,7 +96,7 @@ mov rax, qword ptr [rip + postfix::fn+88] jmp qword ptr [rdi + 8*rax] # TAILCALL ---- -link:https://godbolt.org/clientstate/eNrNU1tr2zAU%2FisHDYpL3Uu8hzL38tAusJfFg%2FZtKUGx5VYgHxlJ9haM99t3ZDup03ZrO1ZoAol0vu9c9Z2GpRqdQMdi%2BN4wtyoFnZjRP1gIT2OpVlWBf4GLUqM3h6PLjBcDmolpJp02W%2FCV487jDZMZ%2FU0IUxxvO4%2B9PU%2B1ujJpF%2BKDxFRVmYBTqa0zghfnc5wjHavUwYwSQDNHoE8tjau4gl%2FeGOxC027bJTqouao8Rr1YB2dwdLLNqbXMoKREufwZWJfF8ZB0B7TddmtPfB2dQ8qVWgwhFnmFqZMag57si9kBpN8QHgccSvfwwTot2X34ObK2DeHloy5KqcSzg95M9ompb2J4k8eij5MjDyi5tP7db%2BisS9%2Bdv7J9auiMfCfHsJ9EnphL5YSxfUqSi6ITvRT1zjJRUEQl7i2KL4Wyo7tcGm5WlzQMMuZc2c5PGkEDrcWISYUW1F6CajUY27a9eTCtN9dt9DLdLjU97KEuBRbC3ens4K4sScNvL%2BxeoZWVeAtdESS%2BTRVxvBZs6UxHvEiSq%2BtF8m06%2Bzq9%2FpJ8DgZBhhCMqKf3sj7%2Fk6zDbo92Hy0IXxiRv2Yv1ivR0zaL8TDqqLzXlvpMpv%2B%2BgtFYJNE%2FrGDDcEjXPamH6p4%2B%2BRSx9p1vqP%2F%2BBlxDKSs%3D[Compare the three calls on Compiler Explorer^] +link:https://godbolt.org/clientstate/eNrtU1tr2zAU%2FisHDYpL3UuchzL38tA2sJfFg%2FZtKUGx5VYgS0KSvQXj%2FfYd2U7ibN3WDLKXNYFEOt%2B56Zzvq0mqpGPSkRg%2B18QtNcMTMeoLCeFlLFWiLORv4EIr6c3h4DKlRY9mbJJxp8wWfO%2Bo83hNeIZ%2FI8QElU9txNGRd7WqNGmb4h2XqSgzBpdcWWcYLa5ncibxWKYOplgA6pkE%2FFTcuJIK%2BOaNwSHUzbadSwcVFaXH8C3WwRWcXWz7VIpnoLFQzr8G1mVx3Bc9AGW3w5oL30cb0EfP81KmjisZdH6%2BjwOQ%2BBvCz7n6rj18sqqIdp95JknThPD6KReaC%2FbHGa%2BH%2BsLA1zm8yWPReHTmAcEX1q%2F8Ec9K%2B9f5KznGB11h7OgcjpPIO%2BZcOGZsVxKZIvCES8K3k4wVmFGwjUXQBRN2cOcLQ83yFoeBxpwK28Zxw3CgFRt4YqMFPi%2BRYtkbm6Z5%2FGFae6ds9DrKLhQu9lRpJgvmnlV28qw10nf%2FnO7IWVoun6BtAsm37iKOV4TVzrSON0ly%2FzBPPk2mHycPH5K7oCdkCMHA9XJD6%2Btf0TpsJXS40UZXcl5xOjcs30UZK1F0bvuTRjRcXvQX0qiJ7Mu1o%2FZQ1bmP3kekeVPOcNrjN%2BXsrpxBql3T%2Fms1jYcLHf93avLf78akGxU%3D[Compare the three calls on Compiler Explorer^] `virtual_ptr` arguments are passed through the method call, to the overrider, which can use them to make further method calls.