From 4c8855eb34450fa1320c4a0d4f7089cb3bb0a2cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20B=C3=B6ck?= Date: Wed, 10 Jun 2026 21:20:30 +0200 Subject: [PATCH 1/3] [hls-fuzzer] Add concept of `weak` dependencies In some circumstances, such as in the limit type system, the transfer functions of an AST node might not care about the specific order of how elements are generated, but rather just *if* a sub element has been generated previously. This is e.g. the case in the limit type system where the total number of statements does not care about whether the sub-statementlist or its statement is generated first, only which has been generated first. This PR therefore adds the new concept of a "weak" dependency to transfer functions which are merely used to affect the transfer function signature: A possibly null pointer and an optional AST node are passed to the transfer function iff that sub-element has been generated already. --- tools/hls-fuzzer/BasicCGenerator.h | 13 ++-- tools/hls-fuzzer/LimitTypeSystem.h | 27 +++++--- tools/hls-fuzzer/TypeSystem.h | 104 ++++++++++++++++++++++++----- 3 files changed, 114 insertions(+), 30 deletions(-) diff --git a/tools/hls-fuzzer/BasicCGenerator.h b/tools/hls-fuzzer/BasicCGenerator.h index 6812336e4..2242a5467 100644 --- a/tools/hls-fuzzer/BasicCGenerator.h +++ b/tools/hls-fuzzer/BasicCGenerator.h @@ -200,11 +200,12 @@ class BasicCGenerator { [&](auto elementIndex, auto &&transferFn) { constexpr std::size_t index = decltype(elementIndex){}; if constexpr (index < sizeof...(SubElements)) { - if (transferFn.getInputDependencies().empty() || - transferFn.getInputDependencies() == - llvm::ArrayRef{INPUT_DEPENDENCY}) { - // No dependency (besides the parent context which is - // satisfied). + if (llvm::all_of( + transferFn.getInputDependencies(), [](std::size_t index) { + return index == INPUT_DEPENDENCY || isWeak(index); + })) { + // No dependency (besides the input context and weak ones which + // are satisfied by default). worklist[workListSize++] = index; return; } @@ -212,7 +213,7 @@ class BasicCGenerator { // Build the outgoing edge list but do keep track of the // number of incoming edges. for (auto fromIndex : transferFn.getInputDependencies()) - if (fromIndex != INPUT_DEPENDENCY) { + if (fromIndex != INPUT_DEPENDENCY && !isWeak(fromIndex)) { forwardEdgeList[fromIndex][forwardEdgeCount[fromIndex]++] = index; ++incomingEdgeCount[index]; diff --git a/tools/hls-fuzzer/LimitTypeSystem.h b/tools/hls-fuzzer/LimitTypeSystem.h index 21018141a..7140786d7 100644 --- a/tools/hls-fuzzer/LimitTypeSystem.h +++ b/tools/hls-fuzzer/LimitTypeSystem.h @@ -135,17 +135,26 @@ class LimitTypeSystem : public TypeSystem { } TransferFnArray getStatementListTransferFns() override { - // TODO: This needlessly forces in which order statements are to be - // generated! - // In reality, the type system does not care whether the statement - // gets generated first or the rest of the statement list, just that - // their respective statement number is propagated to the other. - // This is a missing feature! return { - copyFrom(), - copyFromInput(), + /*statement list=*/copyFirstOf(), + /*statement=*/ + copyFirstOf(), /*output=*/ - copyToOutput(), + OutputTransferFn( + std::index_sequence{}, + [](const ast::StatementList &, LimitTypingContext statement, + const LimitTypingContext &statementList) { + // Regardless of which of the two was generated first, we can + // extract the total number of statements by taking their maximum. + statement.totalNumberOfStatements = + std::max(statement.totalNumberOfStatements, + statementList.totalNumberOfStatements); + return statement; + }), }; } diff --git a/tools/hls-fuzzer/TypeSystem.h b/tools/hls-fuzzer/TypeSystem.h index ecbe9ed3a..8323340d8 100644 --- a/tools/hls-fuzzer/TypeSystem.h +++ b/tools/hls-fuzzer/TypeSystem.h @@ -127,6 +127,26 @@ class OpaqueContext { /// Sentinel value representing a dependency on the input context. constexpr std::size_t INPUT_DEPENDENCY = -1; +/// Marks a dependency as weak. This is a noop for 'INPUT_DEPENDENCY' as it +/// cannot be weak. +constexpr std::size_t weak(std::size_t dependency) { + return dependency | (1ull << (std::numeric_limits::digits - 1)); +} + +/// Returns true if 'dependency' is weak. +constexpr bool isWeak(std::size_t dependency) { + return dependency != INPUT_DEPENDENCY && weak(dependency) == dependency; +} + +/// If 'dependency' is weak, then it returns the original non-weak dependency. +/// Otherwise, returns 'dependency'. +constexpr std::size_t unwrapWeak(std::size_t dependency) { + if (dependency == INPUT_DEPENDENCY) + return INPUT_DEPENDENCY; + + return dependency & ~weak(0); +} + /// Class responsible for telling the generator how to calculate the input /// 'TypingContext' for a given subelement of 'ASTNode'. /// The subelement whose input-context we are calculating for is given by its @@ -139,12 +159,21 @@ constexpr std::size_t INPUT_DEPENDENCY = -1; /// this instance depends on within 'ASTNode::SubElements'. /// The special value 'INPUT_DEPENDENCY' represents depending on the /// input-context of 'ASTNode'. -/// It is the user's responsibility to not create cyclic dependencies. +/// Dependencies can additionally be marked 'weak'. In that case, the elements +/// and contexts will be passed to the transfer function if present, but do not +/// require them to have been generated. +/// +/// It is the user's responsibility to not create cyclic non-weak dependencies. template class TransferFn { template struct CalcCompFn { + using SubElementType = std::tuple_element_t< + std::min(unwrapWeak(current), + std::tuple_size_v - 1), + typename ASTNode::SubElements>; + // Recursive case. using type = typename CalcCompFn< decltype(std::tuple_cat( @@ -155,12 +184,11 @@ class TransferFn { std::tuple, // Add both the context and the ASTNode to the arguments. std::tuple< - const TypingContext &, - const std::tuple_element_t< - std::min(current, std::tuple_size_v< - typename ASTNode::SubElements> - - 1), - typename ASTNode::SubElements> &>>>())), + std::conditional_t, + const std::conditional_t, + SubElementType> &>>>())), remaining...>::type; }; @@ -191,17 +219,28 @@ class TransferFn { /// Specifically, for every element of 'inputIndices' and in the order as /// given in 'inputIndices', the arguments are: /// * The input 'TypingContext' if the value is 'INPUT_DEPENDENCY' - /// * The output 'TypingContext' of the 'i'th subelement of 'ASTNode' followed + /// * If 'i' is not weak, the output 'TypingContext' of the 'i'th subelement + /// of 'ASTNode' followed /// by the subelement's AST node itself. + /// * If 'i' is weak, a pointer to the output 'TypingContext' of the 'i'th + /// subelement of 'ASTNode' or null if not present, followed by an optional + /// of the subelement's AST node itself if already generated. /// /// Example: /// Dependency( + /// ast::BINARY_EXPRESSION::RHS, INPUT_DEPENDENCY>( /// [](const Context& rhsContext, const ast::Expression& rhs, /// const Context& inputContext) -> Context { /// ... /// } /// ) + /// Dependency( + /// [](const Context* rhsContext, const std::optional& rhs, + /// const Context& inputContext) -> Context { + /// ... + /// } + /// ) /// /// The function should always return a 'TypingContext'. All parameters are /// passed as const-references. @@ -220,7 +259,7 @@ class TransferFn { } private: - static_assert(((inputIndices < + static_assert(((unwrapWeak(inputIndices) < std::tuple_size_v || inputIndices == INPUT_DEPENDENCY) && ...), @@ -284,11 +323,19 @@ class OpaqueTransferFn { *reinterpret_cast( std::get - 1>(contexts))); } else { - // Subelement context + ASTNode. - return std::forward_as_tuple( - *reinterpret_cast( - std::get(contexts)), - *std::get(subElements)); + if constexpr (isWeak(index)) { + // Subelement context + ASTNode. + return std::make_tuple( + reinterpret_cast( + std::get(contexts)), + std::cref(std::get(subElements))); + } else { + // Subelement context + ASTNode. + return std::forward_as_tuple( + *reinterpret_cast( + std::get(contexts)), + *std::get(subElements)); + } } }(std::integral_constant{})...); @@ -721,6 +768,33 @@ class TypeSystem : public AbstractTypeSystem { [](const TypingContext &context, auto &&...) { return context; }); } + /// Returns an instance of 'TransferFn' which forwards the first present + /// context from the weak dependencies 'indices'. + template + static auto copyFirstOf() { + return TransferFn([](auto &&...args) { + std::optional result; + foreachInTuples( + [&](auto &&element) { + if (result) + return; + + if constexpr (std::is_same_v, + TypingContext>) { + result = element; + } + if constexpr (std::is_same_v, + const TypingContext *>) { + if (element) + result = *element; + } + }, + std::forward_as_tuple(std::forward(args)...)); + + return std::move(*result); + }); + } + /// Returns a noop 'OutputTransferFn' that keeps the output context /// equal to the input context. template From 9fc641a2e577c0d98aefc8a9ab7c432b250ac06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20B=C3=B6ck?= Date: Mon, 15 Jun 2026 13:59:38 +0200 Subject: [PATCH 2/3] address review comments --- tools/hls-fuzzer/LimitTypeSystem.h | 5 +++-- tools/hls-fuzzer/TypeSystem.h | 25 ++++++++++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/tools/hls-fuzzer/LimitTypeSystem.h b/tools/hls-fuzzer/LimitTypeSystem.h index 7140786d7..34f78157c 100644 --- a/tools/hls-fuzzer/LimitTypeSystem.h +++ b/tools/hls-fuzzer/LimitTypeSystem.h @@ -137,10 +137,11 @@ class LimitTypeSystem : public TypeSystem { TransferFnArray getStatementListTransferFns() override { return { /*statement list=*/copyFirstOf(), /*statement=*/ - copyFirstOf(), /*output=*/ OutputTransferFn( diff --git a/tools/hls-fuzzer/TypeSystem.h b/tools/hls-fuzzer/TypeSystem.h index 8323340d8..c5eceeb75 100644 --- a/tools/hls-fuzzer/TypeSystem.h +++ b/tools/hls-fuzzer/TypeSystem.h @@ -129,17 +129,23 @@ constexpr std::size_t INPUT_DEPENDENCY = -1; /// Marks a dependency as weak. This is a noop for 'INPUT_DEPENDENCY' as it /// cannot be weak. +/// See the 'TypeSystem' documentation for what 'weak' means. constexpr std::size_t weak(std::size_t dependency) { + // We use the top bit being set as an encoding for a dependency being weak. + // Since 'INPUT_DEPENDENCY' is encoded as all 1s, this operation is also a + // noop for 'INPUT_DEPENDENCY'. return dependency | (1ull << (std::numeric_limits::digits - 1)); } /// Returns true if 'dependency' is weak. +/// See the 'TypeSystem' documentation for what 'weak' means. constexpr bool isWeak(std::size_t dependency) { return dependency != INPUT_DEPENDENCY && weak(dependency) == dependency; } /// If 'dependency' is weak, then it returns the original non-weak dependency. /// Otherwise, returns 'dependency'. +/// See the 'TypeSystem' documentation for what 'weak' means. constexpr std::size_t unwrapWeak(std::size_t dependency) { if (dependency == INPUT_DEPENDENCY) return INPUT_DEPENDENCY; @@ -159,9 +165,14 @@ constexpr std::size_t unwrapWeak(std::size_t dependency) { /// this instance depends on within 'ASTNode::SubElements'. /// The special value 'INPUT_DEPENDENCY' represents depending on the /// input-context of 'ASTNode'. -/// Dependencies can additionally be marked 'weak'. In that case, the elements -/// and contexts will be passed to the transfer function if present, but do not -/// require them to have been generated. +/// +/// Dependencies can additionally be marked 'weak'. In that case, the element +/// and context will be passed to the transfer function if and only if they +/// have been generated previously. Otherwise, empty optionals and nullptrs are +/// passed instead. This is the big difference to normal dependencies: They do +/// not force an AST-node to have been generated previously (i.e., do not +/// participate in the topological sort performed by the generator). This makes +/// it legal to have cycles involving weak dependencies. /// /// It is the user's responsibility to not create cyclic non-weak dependencies. template @@ -769,10 +780,14 @@ class TypeSystem : public AbstractTypeSystem { } /// Returns an instance of 'TransferFn' which forwards the first present - /// context from the weak dependencies 'indices'. + /// context from the possibly-weak dependencies in 'indices'. + /// At least one dependency must not be weak. template static auto copyFirstOf() { - return TransferFn([](auto &&...args) { + static_assert((!isWeak(indices) || ...), + "at least one of 'indices' must not be weak"); + + return TransferFn([](auto &&...args) { std::optional result; foreachInTuples( [&](auto &&element) { From bc17edcf33dc28fb0d3c1e6269f1a6ff2a279974 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Markus=20B=C3=B6ck?= Date: Mon, 15 Jun 2026 14:38:52 +0200 Subject: [PATCH 3/3] make the paragraph separetely --- tools/hls-fuzzer/TypeSystem.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tools/hls-fuzzer/TypeSystem.h b/tools/hls-fuzzer/TypeSystem.h index c5eceeb75..7ccdd1f86 100644 --- a/tools/hls-fuzzer/TypeSystem.h +++ b/tools/hls-fuzzer/TypeSystem.h @@ -168,11 +168,13 @@ constexpr std::size_t unwrapWeak(std::size_t dependency) { /// /// Dependencies can additionally be marked 'weak'. In that case, the element /// and context will be passed to the transfer function if and only if they -/// have been generated previously. Otherwise, empty optionals and nullptrs are -/// passed instead. This is the big difference to normal dependencies: They do -/// not force an AST-node to have been generated previously (i.e., do not -/// participate in the topological sort performed by the generator). This makes -/// it legal to have cycles involving weak dependencies. +/// have been generated previously. Otherwise, an empty optional and nullptr +/// are passed for the AST-node and context of that dependency instead. +/// +/// This is the big difference to normal dependencies: They do not force an +/// AST-node to have been generated previously (i.e., do not participate in the +/// topological sort performed by the generator). This makes it legal to have +/// cycles involving weak dependencies. /// /// It is the user's responsibility to not create cyclic non-weak dependencies. template