diff --git a/compiler/propagate/labels.cpp b/compiler/propagate/labels.cpp index 1717378304..a25073a0fd 100644 --- a/compiler/propagate/labels.cpp +++ b/compiler/propagate/labels.cpp @@ -25,6 +25,8 @@ #include "description.hh" #include "global.hh" +#include + using namespace std; //=========================== PATHNAME =============================== @@ -288,3 +290,111 @@ bool matchGroup(Tree gpath, Tree lpath, Tree& rpath) return false; } } + +/** + * @brief Normalize a group label with type prefix against current path. + * This function properly handles relative path components (.., .) in group labels. + * + * @param groupType 0=vertical, 1=horizontal, 2=tab + * @param label The group label (may contain relative path components like "../name") + * @param currentPath The current path context (bottom-up list) + * @return The normalized path to use for contents of this group + */ +Tree normalizeGroupPath(int groupType, Tree label, Tree currentPath) +{ + // Safety check + if (!label) { + return cons(cons(tree(groupType), tree("")), currentPath); + } + + // Convert the label tree to a string + Sym s; + if (!isSym(label->node(), &s)) { + // If it's not a symbol, treat it as pre-parsed + return cons(cons(tree(groupType), label), currentPath); + } + + // Get the label string + const char* labelStr = name(s); + if (!labelStr || labelStr[0] == 0) { + // Empty label + return cons(cons(tree(groupType), tree("")), currentPath); + } + + // Parse the label to extract path components + Tree parsedPath = label2path(labelStr); + + if (!parsedPath || isNil(parsedPath)) { + // Empty parsed path, shouldn't happen but be safe + return cons(cons(tree(groupType), label), currentPath); + } + + // If the parsed path is just a single name (no / separators), + // encode it with the group type and add to current path + if (isNil(tl(parsedPath))) { + Tree name = hd(parsedPath); + // Check if it's a special path component + if (isPathRoot(name) || isPathParent(name) || isPathCurrent(name)) { + // Just apply the path operation + return concatPath(parsedPath, currentPath); + } else { + // It's a simple name, encode it with group type + return cons(cons(tree(groupType), name), currentPath); + } + } + + // The parsed path has multiple components: [elem1, elem2, ..., finalName] + // We need to split off the final name and apply the rest (prefix) to currentPath. + // Strategy: Build a list of all elements except the last, then process them. + + // Collect all elements except the last into a vector for easier processing + std::vector elements; + Tree temp = parsedPath; + while (!isNil(temp)) { + elements.push_back(hd(temp)); + temp = tl(temp); + } + + if (elements.empty()) { + // Should not happen, but be safe + return cons(cons(tree(groupType), label), currentPath); + } + + // The last element is the final name + Tree finalName = elements.back(); + + // Build the prefix path (all elements except the last) in correct order + Tree pathPrefix = gGlobal->nil; + for (size_t i = 0; i + 1 < elements.size(); i++) { + pathPrefix = cons(elements[i], pathPrefix); + } + + // Reverse pathPrefix to get correct top-down order + Tree reversedPrefix = gGlobal->nil; + while (!isNil(pathPrefix)) { + reversedPrefix = cons(hd(pathPrefix), reversedPrefix); + pathPrefix = tl(pathPrefix); + } + + // Apply the prefix to current path (this handles .. and .) + // concatPath resolves relative path elements (.., .) against the current path + Tree adjustedPath = concatPath(reversedPrefix, currentPath); + + // Check if adjustedPath is a bare path marker (not a list) + // This happens when ".." tries to go beyond the root of an empty path. + // In this case, concatPath returns a single pathParent marker. + // We wrap it in a list to maintain the path structure invariant. + if (adjustedPath && !isNil(adjustedPath) && !isList(adjustedPath)) { + // adjustedPath is a single marker (like pathParent), wrap it in a list + adjustedPath = cons(adjustedPath, gGlobal->nil); + } + + // Now add the final name with group type encoding + if (isPathRoot(finalName) || isPathParent(finalName) || isPathCurrent(finalName)) { + // This shouldn't normally happen (path ending in .. or .), but handle it + return concatPath(cons(finalName, gGlobal->nil), adjustedPath); + } else { + // Encode the final name with the group type + return cons(cons(tree(groupType), finalName), adjustedPath); + } +} diff --git a/compiler/propagate/labels.hh b/compiler/propagate/labels.hh index 46828c5069..0bd5c6048f 100644 --- a/compiler/propagate/labels.hh +++ b/compiler/propagate/labels.hh @@ -34,4 +34,8 @@ Tree normalizePath(Tree path); Tree superNormalizePath(Tree path); bool matchGroup(Tree gpath, Tree lpath, Tree& rpath); +// Normalize a group label with type prefix against current path +// groupType: 0=vertical, 1=horizontal, 2=tab +Tree normalizeGroupPath(int groupType, Tree label, Tree currentPath); + #endif diff --git a/compiler/propagate/propagate.cpp b/compiler/propagate/propagate.cpp index d18a9ca66e..ce99e58c27 100644 --- a/compiler/propagate/propagate.cpp +++ b/compiler/propagate/propagate.cpp @@ -431,15 +431,15 @@ static siglist realPropagate(Tree slotenv, Tree path, Tree box, const siglist& l // User Interface Groups else if (isBoxVGroup(box, label, t1)) { - return propagate(slotenv, cons(cons(tree(0), label), path), t1, lsig); + return propagate(slotenv, normalizeGroupPath(0, label, path), t1, lsig); } else if (isBoxHGroup(box, label, t1)) { - return propagate(slotenv, cons(cons(tree(1), label), path), t1, lsig); + return propagate(slotenv, normalizeGroupPath(1, label, path), t1, lsig); } else if (isBoxTGroup(box, label, t1)) { - return propagate(slotenv, cons(cons(tree(2), label), path), t1, lsig); + return propagate(slotenv, normalizeGroupPath(2, label, path), t1, lsig); } // Block Diagram Composition Algebra diff --git a/tests/codegen-tests/relative-group-paths-string.dsp b/tests/codegen-tests/relative-group-paths-string.dsp new file mode 100644 index 0000000000..4487849b78 --- /dev/null +++ b/tests/codegen-tests/relative-group-paths-string.dsp @@ -0,0 +1,11 @@ +// Companion test for relative-group-paths.dsp +// This uses the string literal form that was already working +// Both tests should produce identical UI structure + +import("stdfaust.lib"); + +// Using explicit path in slider string (already working) +freq = vslider("../h:FREQ/Freq",200,200,1000,0.01); + +// The freq slider should be shared between V1 and V2 +process = hgroup("V1", os.osc(freq)), hgroup("V2", os.osc(freq)); diff --git a/tests/codegen-tests/relative-group-paths.dsp b/tests/codegen-tests/relative-group-paths.dsp new file mode 100644 index 0000000000..4cb1d82161 --- /dev/null +++ b/tests/codegen-tests/relative-group-paths.dsp @@ -0,0 +1,20 @@ +// Regression test for issue #1063: relative paths in group labels +// https://github.com/grame-cncm/faust/issues/1063 +// +// This test verifies that relative path components (.., .) in group labels +// are resolved correctly against the parent path context. +// +// Both forms should produce the same resolved label for the slider: +// 1. Using explicit path in slider: vslider("../h:FREQ/Freq", ...) +// 2. Using hgroup with relative path: hgroup("../FREQ", vslider("Freq", ...)) +// +// Expected behavior: In both cases, the slider should be shared between +// V1 and V2 groups, appearing in a FREQ group at the same level as V1/V2. + +import("stdfaust.lib"); + +// Using hgroup with relative path (previously broken, now fixed) +freq = hgroup("../FREQ", vslider("Freq",200,200,1000,0.01)); + +// The freq slider should be shared between V1 and V2 +process = hgroup("V1", os.osc(freq)), hgroup("V2", os.osc(freq)); diff --git a/tests/codegen-tests/relative_group_paths_explicit.dsp b/tests/codegen-tests/relative_group_paths_explicit.dsp new file mode 100644 index 0000000000..958bfae396 --- /dev/null +++ b/tests/codegen-tests/relative_group_paths_explicit.dsp @@ -0,0 +1,5 @@ +import("stdfaust.lib"); + +// Explicit path form that previously worked +freq_explicit = vslider("../h:FREQ/Freq", 200, 200, 1000, 0.01); +process = hgroup("V1", os.osc(freq_explicit)), hgroup("V2", os.osc(freq_explicit)); \ No newline at end of file diff --git a/tests/codegen-tests/relative_group_paths_grouped.dsp b/tests/codegen-tests/relative_group_paths_grouped.dsp new file mode 100644 index 0000000000..5ec6dc65ee --- /dev/null +++ b/tests/codegen-tests/relative_group_paths_grouped.dsp @@ -0,0 +1,5 @@ +import("stdfaust.lib"); + +// Relative-group form that previously failed: group uses relative path segment +freq_grouped = hgroup("../FREQ", vslider("Freq", 200, 200, 1000, 0.01)); +process = hgroup("V1", os.osc(freq_grouped)), hgroup("V2", os.osc(freq_grouped));