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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,14 @@ Each of these was reproduced before it was fixed, and the gate that already owns
for the first `(` anywhere in the statement. The field vanished while the struct reported `modeled="1"` and a size
short by its bytes. Only a `(` before the first `[`, `=`, `{` or bitfield `:` now opens a parameter list, and an
`operator` member is still a function. Gate: `test/layoutcheck.sh` §13.
- **`--layout` dropped a data member whose declaration carries a `(` that belongs to an `alignas`,
`__attribute__` or `decltype` specifier, or sits inside a template argument list, and still said the size was
right.** `alignas(8) int x`, `int x __attribute__((aligned(8)))`, `decltype(1) x` and `std::function<void(int)>
cb` were all taken for member functions too, for the same reason as the row above: the scan still looked at the
first `(` in the statement, whichever `(` that was. The field vanished while the struct reported `modeled="1"`
and a size short by its bytes. That first `(` is now skipped when it opens one of those specifiers or sits
inside `<…>`; each shape now comes back refused (`modeled="0"`, a named caveat) instead of silently missing.
Gate: `test/layoutcheck.sh` §14.
- **`--eval-skills` aborted on a skills directory it could not fully read.** A `SKILL.md` symlinked to itself, a
directory link loop or a mode-000 skill raised an uncaught `filesystem_error` from the throwing
`std::filesystem` overloads (exit 134). The walk now uses the `error_code` forms, skips an unreadable entry, the
Expand Down
55 changes: 53 additions & 2 deletions src/layout.h
Original file line number Diff line number Diff line change
Expand Up @@ -1752,17 +1752,68 @@ inline void appendField( BodyWalk& w, const Declarator& d, std::string_view type
w.def.fields.push_back( std::move( f ) );
}

// True when the word immediately before `s[at]` — the `(` at `at` — spells `alignas` / `__attribute__` /
// `decltype`: that `(` opens the specifier's own argument list, not a member's parameter list.
inline bool opensAttrSpecifier( std::string_view s, std::size_t at )
{
static constexpr std::string_view kAttrKeywords[] = { "alignas", "__attribute__", "decltype" };
std::size_t wordEnd = at;
while( wordEnd > 0 && std::isspace( (unsigned char)s[wordEnd - 1] ) != 0 ) { --wordEnd; }
std::size_t wordStart = wordEnd;
while( wordStart > 0 && identByte( (unsigned char)s[wordStart - 1] ) ) { --wordStart; }
const std::string_view word = s.substr( wordStart, wordEnd - wordStart );
for( std::string_view kw : kAttrKeywords )
{
if( word == kw ) { return true; }
}
return false;
}

// The statement forms that contribute NO storage and are simply skipped, plus the ones that withdraw the
// numbers. Returns true when the statement was consumed here and holds no field declarators.
// The first `(` that is a CANDIDATE for a member declaration's parameter list: skip one that instead
// belongs to an `alignas( … )` / `__attribute__( ( … ) )` / `decltype( … )` specifier, or that sits inside a
// template argument list's `<…>` (`std::function< void(int) >`). None of those opens a parameter list, and
// counting one anyway silently dropped the field it decorates while the aggregate still reported
// modeled="1": `alignas(8) int x`, `int x __attribute__((aligned(8)))`, `decltype(1) x` and
// `std::function<void(int)> cb` each lost their field this way. Returns npos when no candidate remains.
inline std::size_t candidateParen( std::string_view s )
{
int angle = 0;
for( std::size_t i = 0; i < s.size(); )
{
const char c = s[i];
if( c == '<' ) { ++angle; ++i; continue; }
if( c == '>' && angle > 0 ) { --angle; ++i; continue; }
if( c != '(' ) { ++i; continue; }
if( angle > 0 ) { ++i; continue; } // a template argument's own parens — not a parameter list
if( !opensAttrSpecifier( s, i ) )
{
return i;
}
const std::size_t close = matchBracket( s, i, '(', ')' );
if( close == std::string_view::npos )
{
return std::string_view::npos; // unbalanced — degrade rather than misclassify
}
i = close;
}
return std::string_view::npos;
}

// Where a member declaration's parameter list opens, or npos when it has none. Only a `(` that comes BEFORE the first
// `[`, `=`, `{` or bitfield `:` can open one: `char a[(4)];`, `int x = (3);` and `int x{ (3) };` are data members whose
// parenthesis sits in an extent or an initializer. Reading those as member functions dropped the field from the layout
// while the struct still reported modeled="1" and a size four bytes short. `operator=`, `operator[]` and `operator()`
// are functions whose own name holds one of those characters, so an `operator` word decides first.
inline std::size_t parameterListParen( std::string_view s )
{
const std::size_t paren = s.find( '(' );
if( paren == std::string_view::npos || containsWord( s, "operator" ) )
if( containsWord( s, "operator" ) )
{
return s.find( '(' );
}
const std::size_t paren = candidateParen( s );
if( paren == std::string_view::npos )
{
return paren;
}
Expand Down
31 changes: 31 additions & 0 deletions test/layoutcheck.sh
Original file line number Diff line number Diff line change
Expand Up @@ -306,5 +306,36 @@ for pair in ParenExtent:8 ParenInit:8 ParenBrace:8 OperatorAssign:8; do
|| no "$s: fields=${fields:-?} size=${got:-?} (want 2 fields, size $want): $( grep -o '<def .*</def>' "$TMP/pf_$s" | head -c 200 )"
done

# ── 14) a `(` from alignas/__attribute__/decltype, or one inside a template's `<…>`, is not a parameter
# list either — counting it as one silently dropped the field it decorates while the struct still
# said modeled="1" with a size short by exactly that field's bytes.
expect_refused AlignasFieldCase unknown-type
expect_refused AttributeFieldCase unparsed-member
expect_refused DecltypeFieldCase unknown-type
expect_refused StdFunctionFieldCase unknown-type

run AlignasFieldCase
has 'f n="x"' \
&& ok "AlignasFieldCase: the alignas-decorated field is still COUNTED (not silently dropped)" \
|| no "AlignasFieldCase: field 'x' vanished with no trace: $( printf '%s' "$L" | tr '<' '\n' | grep '^f ' )"
{ [ "$( field n sz )" = "4" ] && [ "$( field c sz )" = "1" ]; } \
&& ok "AlignasFieldCase: the plain neighbours (n, c) still size normally" \
|| no "AlignasFieldCase: a neighbour field lost its size: n=$( field n sz ) c=$( field c sz )"

run AttributeFieldCase
has 'caveat k="unparsed-member" d="int x __attribute__' \
&& ok "AttributeFieldCase: the refusal NAMES the dropped declaration text, not a silent size" \
|| no "AttributeFieldCase: caveat detail did not name the field: $( printf '%s' "$L" | tr '<' '\n' | grep '^caveat' )"

run DecltypeFieldCase
has 'f n="x"' \
&& ok "DecltypeFieldCase: the decltype field is still COUNTED (not silently dropped)" \
|| no "DecltypeFieldCase: field 'x' vanished with no trace: $( printf '%s' "$L" | tr '<' '\n' | grep '^f ' )"

run StdFunctionFieldCase
has 'f n="cb"' \
&& ok "StdFunctionFieldCase: the std::function field is still COUNTED (not silently dropped)" \
|| no "StdFunctionFieldCase: field 'cb' vanished with no trace: $( printf '%s' "$L" | tr '<' '\n' | grep '^f ' )"

[ $fail -eq 0 ] && echo "layoutcheck: ALL PASS" || echo "layoutcheck: FAILURES"
exit $fail
38 changes: 38 additions & 0 deletions test/layoutfix/attrfields.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#pragma once

// attrfields.h — layout fixture: a field whose declaration carries a `(` that is NOT a parameter list —
// `alignas(N)`, `__attribute__((...))`, `decltype(...)` or a `std::function<...>`'s template-nested paren.
// Never compiled; ripwire indexes it as C++ and test/layoutcheck.sh asserts the computed table.
//
// parameterListParen (src/layout.h) used to take the FIRST `(` in the statement as a member-function's
// parameter list, whichever `(` that was. All four shapes below put an unrelated `(` before the real
// field, so the field was read as a member function and dropped — while the struct still reported
// modeled="1" with a size short by exactly that field's bytes. Each must now come back REFUSED
// (modeled="0", a named caveat, the field still counted) rather than silently missing.

struct AlignasFieldCase
{
int n;
alignas( 8 ) int x;
char c;
};

struct AttributeFieldCase
{
int n;
int x __attribute__( ( aligned( 8 ) ) );
char c;
};

struct DecltypeFieldCase
{
int n;
decltype( 1 ) x;
};

#include <functional>
struct StdFunctionFieldCase
{
int n;
std::function<void(int)> cb;
};
Loading