Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
366 changes: 364 additions & 2 deletions src/welder/rods/csharp/document/artifacts.hpp

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions src/welder/rods/csharp/document/class_writer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,14 @@ struct class_writer {
std::vector<std::string> surface_names{};
std::vector<std::string> nested_names{};
std::string members{}; /**< Accumulated property/method/ctor text. */
/** Whether this class carries the rod's
`[[=welder::rods::csharp::family_surface]]` opt-in — the render-time
family synthesis hoists onto a base ONLY when the base is marked. */
bool family_marked{false};
/** The member manifest the family-surface synthesis intersects —
recorded by the field/method emitters beside their emissions, flushed
into @ref document::family_records with the class's identities. */
std::vector<family_member> family_members{};

/** One recorded comparison-operator emission, held back until flush: C#
requires `==`/`!=`, `<`/`>` and `<=`/`>=` in PAIRS, so pairing is
Expand Down Expand Up @@ -130,6 +138,8 @@ struct class_writer {
surface_names = std::move(o.surface_names);
nested_names = std::move(o.nested_names);
members = std::move(o.members);
family_marked = o.family_marked;
family_members = std::move(o.family_members);
comparisons = std::move(o.comparisons);
indexer_sigs = std::move(o.indexer_sigs);
o.doc = nullptr;
Expand Down Expand Up @@ -355,6 +365,14 @@ struct class_writer {
// into its owner's buffer and is never a boundary of its own.
if (!sink)
doc->section(cs_ns).breaks.push_back(out.size());
// The class's family manifest, for the render-time family-surface
// synthesis ([[=welder::rods::csharp::family_surface]]). Every
// class records one — an unmarked class's manifest still resolves
// member types when it appears INSIDE a marked family's members.
doc->family_records.push_back(
{cs_path, cs_ns, cs_name, base_ref, sink != nullptr,
family_marked, std::move(surface_names), std::move(nested_names),
std::move(family_members)});
}
};

Expand Down
9 changes: 9 additions & 0 deletions src/welder/rods/csharp/emit/classes.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

#include <welder/rods/csharp/directors.hpp>
#include <welder/rods/csharp/document.hpp>
#include <welder/rods/csharp/marks.hpp>
#include <welder/rods/csharp/emit/directors.hpp>
#include <welder/rods/csharp/emit/refs.hpp>
#include <welder/rods/csharp/emit/spellings.hpp>
Expand Down Expand Up @@ -122,6 +123,14 @@ class class_opener {
w.handle_field = "_h_" + sanitized(w.cs_path);
w.handle_cs = w.cs_path + "Handle";
w.destroy_symbol = w.sym_prefix + "_destroy";
// The family-surface OPT-IN: only a base carrying the rod's own
// [[=welder::rods::csharp::family_surface]] mark has a
// version-agnostic surface synthesized onto it — synthesizing
// members onto a base is too intrusive to infer from structure alone.
{
constexpr bool fm{family_surface_marked(std::meta::dealias(^^T))};
w.family_marked = fm;
}
finish<T, Bases>(w);
return w;
}
Expand Down
60 changes: 60 additions & 0 deletions src/welder/rods/csharp/emit/fields.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class field_emitter {
template <std::meta::info Mem, class Style = ::welder::naming::none>
void emit_field() {
constexpr std::meta::info MT{std::meta::type_of(Mem)};
record_family_field<Mem, Style>();
// A non-const SCALAR/ENUM sequence member is a LIVE object, so it
// binds by reference like its welded-element siblings — a generated
// wrapper with live element access and a zero-copy AsSpan() — never a
Expand Down Expand Up @@ -167,6 +168,18 @@ class field_emitter {
std::meta::remove_cvref(std::meta::return_type_of(Getter))};
constexpr bool checked{(require_marshallable(RT, true), true)};
static_assert(checked);
{
constexpr bool has_setter{Setter != std::meta::info{}};
family_member fm{};
fm.name = name;
fm.type_str = public_type<RT, ::welder::naming::none>();
fm.settable = has_setter;
fm.handle_like = classify(RT) == marshal_kind::handle;
fm.elem_ref = welded_seq_elem_ref<RT>();
if (const char* d{::welder::doc_of<Getter>()}; d && *d)
fm.doc = d;
_writer.family_members.push_back(std::move(fm));
}
const std::string gid{std::meta::identifier_of(Getter)};
const std::string glookup{
"wcs::named_member(" +
Expand Down Expand Up @@ -214,6 +227,53 @@ class field_emitter {
}

private:
/** The element's type placeholder when @a MT is a sequence of WELDED
elements (the shape the family surface hoists as a
`FamilyVector<ElementBase>` view), else empty.
@tparam MT the member/return type reflection.
@return the element's `\x01raw\x02` reference, or empty. */
template <std::meta::info MT>
static std::string welded_seq_elem_ref() {
if constexpr (classify(MT) == marshal_kind::seq_ref) {
constexpr std::meta::info El{
std::meta::remove_cvref(sequence_element(bare(MT)))};
if constexpr (classify(El) == marshal_kind::handle)
return type_ref<bare(El)>();
}
return {};
}

/** Record data member @a Mem into the class's family manifest: its
resolved C# name, public type spelling, settability and doc — exactly
what the render-time family synthesis intersects across a MARKED
family's concretes (an unrelated class's manifest still resolves
member types when it appears inside a marked family's members).
@tparam Mem a reflection of the data member.
@tparam Style the name style. */
template <std::meta::info Mem, class Style>
void record_family_field() {
constexpr std::meta::info MT{std::meta::type_of(Mem)};
family_member fm{};
fm.name = ::welder::name_of<Mem, cs, Style,
::welder::ent_kind::field>();
if constexpr (classify(MT) == marshal_kind::seq_value &&
!std::meta::is_const_type(MT)) {
// The live-wrapper form emit_scalar_seq binds (its type is the
// generated container wrapper, not the by-value T[]).
fm.type_str = container_ref<bare(MT)>();
fm.settable = !::welder::member_no_reassign(Mem, cs);
} else {
fm.type_str = public_type<MT, Style>();
fm.settable = !(std::meta::is_const_type(MT) ||
::welder::member_no_reassign(Mem, cs));
fm.handle_like = classify(MT) == marshal_kind::handle;
fm.elem_ref = welded_seq_elem_ref<MT>();
}
if (const char* d{::welder::doc_of<Mem>()}; d && *d)
fm.doc = d;
_writer.family_members.push_back(std::move(fm));
}

/** The bespoke per-member setter (thunk + P/Invoke): the member's own
`operator=` spliced through `field_set`. Shared by the bespoke path
and by the erased path's handle-like members, whose GETTER erases to
Expand Down
26 changes: 26 additions & 0 deletions src/welder/rods/csharp/emit/methods.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ class method_emitter {
_writer.handle_cs, _writer.handle_field}
.emit();
}
record_family_method<fn, Style>();
}
}

Expand Down Expand Up @@ -166,6 +167,31 @@ class method_emitter {
}

private:
/** Record overload @a Fn into the class's family manifest: the group's
resolved C# name, the public parameter list and return spelling, and
the forwarding argument names — what the render-time family synthesis
needs to hoist an identically-spelled overload onto a MARKED family
base as a dispatch method.
@tparam Fn a reflection of the member function.
@tparam Style the name style. */
template <std::meta::info Fn, class Style>
void record_family_method() {
constexpr std::size_t n{std::meta::parameters_of(Fn).size()};
const call_pieces cp{
build_params<Fn, Style>(std::make_index_sequence<n>{})};
family_member fm{};
fm.method = true;
fm.name = _group_name;
fm.ret_str =
public_return_type<std::meta::return_type_of(Fn), Style>();
fm.params_decl = cp.wrapper_params;
for (const std::string& p : split_param_names(cp.param_names))
fm.args += (fm.args.empty() ? "" : ", ") + p;
if (const char* d{::welder::doc_of<Fn>()}; d && *d)
fm.doc = d;
_writer.family_members.push_back(std::move(fm));
}

/** Emit virtual slot @a Fn: the ordinary (virtual-dispatch) thunk, a
qualified base-call thunk, P/Invokes for both, and one `public virtual`
wrapper branching on `_isDirector`. The slot's C# name was already
Expand Down
60 changes: 60 additions & 0 deletions src/welder/rods/csharp/marks.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#pragma once
#include <meta>

/** @file
The C# rod's OWN annotation vocabulary — opt-ins that are meta to the C#
binding rather than to the welded C++ surface, so they live in the rod and
never touch welder core. One mark today: @ref
welder::rods::csharp::family_surface, the family-surface opt-in.

A consumer whose welded headers must also parse WITHOUT this rod (a
Python-only build of the same project, where welder-csharp is not even
fetched) spells the mark behind a macro that expands to nothing when the
rod is absent — the annotation then simply never exists in those builds:
@code
#if defined(MYPROJ_WITH_CSHARP_ROD)
#include <welder/rods/csharp/marks.hpp>
#define MYPROJ_CS_FAMILY_SURFACE =welder::rods::csharp::family_surface,
#else
#define MYPROJ_CS_FAMILY_SURFACE
#endif

struct [[
=welder::weld,
MYPROJ_CS_FAMILY_SURFACE
=welder::doc("...")
]] EntityBase {};
@endcode
The macro (with its trailing comma) must expand consistently across every
TU of one build tree — drive it from the build system, not from
`__has_include`, so the generator, the shim and the library agree on the
class's annotation list.
*/

namespace welder::inline v0::rods::csharp {

/** The stored form of the @ref family_surface mark. */
struct family_surface_spec {};

/** The family-surface OPT-IN: placed on a welded BASE class, it opts the
base's family — two or more welded classes deriving it, the shape a
versioned class template welded per instantiation makes — into the
rod-synthesized version-agnostic surface ON the base (the member
intersection the derived classes bind identically, as dispatch members;
see the document assembler's family synthesis). Synthesizing members onto
a base is too intrusive to infer from structure alone, so the mark is
strictly required: an unmarked base is never touched.
@code
struct [[=welder::weld, =welder::rods::csharp::family_surface]] Base {};
@endcode */
inline constexpr family_surface_spec family_surface{};

/** Does @a type carry the @ref family_surface opt-in?
@param type a reflection of the welded base class to test.
@return `true` iff the mark is present. */
consteval bool family_surface_marked(std::meta::info type) {
return !std::meta::annotations_of_with_type(type, ^^family_surface_spec)
.empty();
}

} // namespace welder::inline v0::rods::csharp
14 changes: 14 additions & 0 deletions tests/csharp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,16 @@ welder_csharp_generate_bindings(welder_test_cs_namespace
OUTPUT_DIR ${_ws}/namespace
DEPENDS ${WELDER_TEST_COMMON_CPP}/namespace.hpp)

# The version-family synthesis: per-era template instantiations sharing a
# welded base marked [[=welder::rods::csharp::family_surface]] gain dispatch
# members ON the base (an unmarked base gains nothing — the opt-in default).
welder_csharp_generate_bindings(welder_test_cs_family
SOURCES cpp/gen_family.cpp
LIBRARY welder_test_cs_family
INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/cpp
OUTPUT_DIR ${_ws}/family
DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/cpp/family.hpp)

# Building the targets both regenerates the artifacts and compiles the shims —
# the compile IS a test (the generated splices must re-derive every member).
add_test(NAME csharp.build
Expand All @@ -136,6 +146,7 @@ add_test(NAME csharp.build
welder_test_cs_enums welder_test_cs_overloads
welder_test_cs_properties welder_test_cs_naming
welder_test_cs_nested welder_test_cs_namespace
welder_test_cs_family
--config $<CONFIG>)
set_tests_properties(csharp.build PROPERTIES FIXTURES_SETUP csharp_native)

Expand Down Expand Up @@ -222,6 +233,8 @@ file(GENERATE OUTPUT ${_app}/app.csproj CONTENT
<Compile Include=\"${_ws}/naming/Bindings.cs\" />
<Compile Include=\"${_ws}/nested/Bindings.cs\" />
<Compile Include=\"${_ws}/namespace/Bindings.cs\" />
<Compile Include=\"${_ws}/family/Bindings.cs\" />
<Compile Include=\"${CMAKE_CURRENT_SOURCE_DIR}/app/FamilyTests.cs\" />
<None Include=\"$<TARGET_FILE:welder_test_csharp>\" CopyToOutputDirectory=\"PreserveNewest\" />
<None Include=\"$<TARGET_FILE:welder_test_cs_retpolicy>\" CopyToOutputDirectory=\"PreserveNewest\" />
<None Include=\"$<TARGET_FILE:welder_test_cs_operators>\" CopyToOutputDirectory=\"PreserveNewest\" />
Expand All @@ -237,6 +250,7 @@ file(GENERATE OUTPUT ${_app}/app.csproj CONTENT
<None Include=\"$<TARGET_FILE:welder_test_cs_naming>\" CopyToOutputDirectory=\"PreserveNewest\" />
<None Include=\"$<TARGET_FILE:welder_test_cs_nested>\" CopyToOutputDirectory=\"PreserveNewest\" />
<None Include=\"$<TARGET_FILE:welder_test_cs_namespace>\" CopyToOutputDirectory=\"PreserveNewest\" />
<None Include=\"$<TARGET_FILE:welder_test_cs_family>\" CopyToOutputDirectory=\"PreserveNewest\" />
</ItemGroup>
</Project>
")
Expand Down
Loading
Loading