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
45 changes: 45 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -687,6 +687,8 @@ tables-block: build/schema_test_block build/schema_test_block_asan build/schema_
./build/schema_test_block
./build/schema_test_block_asan
./build/schema_test_block_tsan
$(MAKE) tables-block-const
$(MAKE) tables-block-const-negative-control
# THE C# HALF of this two-language gate runs unless the cs leg is named in
# SCHEMA_SKIP_LEGS (issue #599). A skip that reaches the leg loop and not here
# is a skip that stops the chain anyway, on the same missing dotnet.
Expand All @@ -696,6 +698,49 @@ else
@echo "tables-block: the C# half is SKIPPED, SCHEMA_SKIP_LEGS names the cs leg"
endif

# THE CONST READ PATH (docs/SPEC-TABLES.md §19.2, schema#455). A block is
# memory another build wrote, so the consumer half of the form reads bytes it
# does not own: a pinned image is loaded, held as a `const uint8_t *` from that
# moment, and opened WITH NO CAST through the const overload. The rows come
# back read-only, and the same bytes opened writable read the same values, so
# the producer's path is proved unchanged rather than assumed.
#
# It runs SANITIZED beside the plain build on #277's rule: the const view
# points into a buffer sized to the image, so a row read past the extent lands
# in a redzone.
build/schema_test_block_const: build/tables-generated/.stamp test/tables/block_const_main.cpp
@mkdir -p build
$(CXX) $(BLOCK_CXXFLAGS) $(BLOCK_INCLUDES) test/tables/block_const_main.cpp $(BLOCK_SOURCES) -o $@

build/schema_test_block_const_asan: build/tables-generated/.stamp test/tables/block_const_main.cpp
@mkdir -p build
$(CXX) $(BLOCK_CXXFLAGS) -fsanitize=address,undefined -fno-sanitize-recover=all \
-fno-omit-frame-pointer -g $(BLOCK_INCLUDES) test/tables/block_const_main.cpp $(BLOCK_SOURCES) -o $@

.PHONY: tables-block-const
tables-block-const: build/schema_test_block_const build/schema_test_block_const_asan
./build/schema_test_block_const
./build/schema_test_block_const_asan

# ITS NEGATIVE COMPILE CONTROL, and it is the half a run cannot hold: a view
# handing back a reference a consumer can WRITE through is a read-only view in
# name only, and no green run of the gate above would ever say so. The same
# program, one assignment through the const view added by -DBLOCK_CONST_WRITE,
# and the COMPILE must go red, on the const qualification and not on some
# other error, which is what the second grep is for.
.PHONY: tables-block-const-negative-control
tables-block-const-negative-control: build/tables-generated/.stamp test/tables/block_const_main.cpp
@mkdir -p build
@if $(CXX) $(BLOCK_CXXFLAGS) $(BLOCK_INCLUDES) -DBLOCK_CONST_WRITE -fsyntax-only \
test/tables/block_const_main.cpp > build/block-const-write.log 2>&1; then \
echo "NEGATIVE CONTROL FAILED: a write through the const row view COMPILED"; exit 1; \
fi
@grep -qE "read-only|not assignable|const-qualified|assignment of member" build/block-const-write.log || \
{ echo "NEGATIVE CONTROL FAILED: the compile went red, but not on the const view"; \
cat build/block-const-write.log; exit 1; }
@grep -m1 -E "read-only|not assignable|const-qualified|assignment of member" build/block-const-write.log
@echo "negative control: one write through the const row view turns the COMPILE red"

# ---------------------------------------------------------------------------
# THE FORGERY FUZZER (docs/SPEC-TABLES.md §19.2, §19.5). The hand-written battery in
# block_main.cpp and Program.cs is eleven forgeries, one per fact BlockOpen
Expand Down
31 changes: 31 additions & 0 deletions docs/SPEC-TABLES.md
Original file line number Diff line number Diff line change
Expand Up @@ -13623,6 +13623,26 @@ schema name, as everywhere else in that backend.
so that the base's alignment can be the last clause. A block whose bytes
are fewer than its prologue answers `truncated`, and a null base
`unaligned_base`, on the readings §7 gives those two.

**THE READ SIDE TAKES A CONST OVERLOAD, and the surface SPLITS the way C#'s
already does.** A block is memory another build wrote, so a consumer opens
bytes it did not produce: it reads them and writes nothing, and a read-only
mapping is not something a loader should have to `const_cast` to open. So
`bool <Name>BlockOpen( <Name>Block::Const & block, const void * base,
int64_t bytes, TableRefuseReason * reason = NULL )` sits beside the mutable
one. `<Name>Block::Const` is a MEMBER TYPE of the handle above and claims no
name of its own (§11); it carries the same three facts with `const` on both
pointers, and the row accessors overloaded on it answer the CONST VIEWS,
`TableBlockConstRows<Row>` and `TableBlockConstSpan<Row>`, whose iterators,
spans and typed base all hand back `const Row`. **THE CHECK IS ONE BODY**:
both overloads run the same clauses in the same order and name the same
reason on the same refusal, because a check reads and never writes, so the
two paths cannot drift. **The PRODUCER's path is unchanged** in every part:
`Begin`, the fill accessors and the typed base a worker indexes are what
they were, and a producer holds a mutable block exactly as before. The split
is the one C# states above with `ref readonly` and `ReadOnlySpan<Row>`, and
the one Rust has by returning `&[Row]`. **A write through a const view is a
COMPILE ERROR**, held by a negative compile control (§19.5).
- **An array is ITERATED, not indexed by hand.** The accessor yields a
reference to each row where it lies, at the pitch the instance gives, for
`count` rows — a range-for in C++, an enumerator in C#, the equivalent per
Expand Down Expand Up @@ -13835,6 +13855,17 @@ difference between a form and a convention.
offset in the compiler's layout model and the generated asserts go red on
both backends. A layout test that shares its layout model with the code it
checks proves nothing, and these two are what separate them.
- **THE CONST READ PATH, and its NEGATIVE COMPILE CONTROL** (§19.2). A pinned
block image is loaded, held as a `const uint8_t *` from that moment and
opened with no cast through the const overload; its rows are walked through
`TableBlockConstRows` and `TableBlockConstSpan` and compared, value for
value, against the same bytes opened writable, so the two overloads agree
and the producer's path is proved unchanged rather than assumed. The refusal
clauses answer on the const overload too, a null base and a short buffer and
an unaligned base each naming what §19.2 gives it. The CONTROL is the half a
run cannot hold: the same program with one assignment through the const view
must FAIL TO COMPILE, on the const qualification, because a read-only view
whose writes compile is a view in name only.
- **A `bool` row.** A row type carrying two `bool`s beside its scalars, whose
C# size and offsets are asserted under the managed model (§19.3) — the case
where the two C# layout models disagree, pinned so a port cannot pick the
Expand Down
94 changes: 86 additions & 8 deletions generated/bench/tables/cpp/BenchTableBlock.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,19 @@ namespace benchtable {

// ---- the block form of table TableEntity: the open path and the descriptors ----

bool TableEntityBlockOpen( TableEntityBlock & block, void * base, int64_t bytes, TableRefuseReason * reason )
// THE CHECK ITSELF, over a base it never writes through, because a check
// READS (docs/SPEC-TABLES.md §19.2). Both overloads of TableEntityBlockOpen are
// this one body, so the producer's path and the consumer's cannot drift:
// the same clauses in the same order, and the same reason on the same
// refusal, whether the caller handed over bytes it owns or bytes it may
// only read. It answers the USED EXTENT beside the verdict, which is the
// one thing each overload has to write into its own handle.
//
// It sits in an anonymous namespace and claims no name (§11).
namespace {

bool tableentity_block_open_check( const void * base, int64_t bytes, int64_t * used_out, TableRefuseReason * reason )
{
block.base = NULL;
block.projection = NULL;
block.bytes = 0;
// a null buffer is the CALLER's defect, as an unaligned base is; a buffer
// shorter than the prologue has no prologue to read and is truncated
if ( base == NULL ) { return TableCookRefuse( reason, unaligned_base ) != NULL; }
Expand Down Expand Up @@ -49,12 +57,43 @@ bool TableEntityBlockOpen( TableEntityBlock & block, void * base, int64_t bytes,
// the base's alignment, LAST: the only clause that reads nothing out of
// the block, and the caller's defect rather than the file's
if ( ( (uintptr_t) base % 64 ) != 0 ) { return TableCookRefuse( reason, unaligned_base ) != NULL; }
*used_out = used;
return true;
}

} // namespace

// THE PRODUCER's overload, unchanged in what it does: the check above, and
// then a handle over bytes the caller may write.
bool TableEntityBlockOpen( TableEntityBlock & block, void * base, int64_t bytes, TableRefuseReason * reason )
{
block.base = NULL;
block.projection = NULL;
block.bytes = 0;
int64_t used = 0;
if ( !tableentity_block_open_check( base, bytes, &used, reason ) ) { return false; }
block.base = (uint8_t *) base;
block.projection = (TableEntityBlock::Projection *) base;
block.bytes = used;
return true;
}

// THE CONSUMER's overload (schema#455): the same check, and then a handle
// that carries const the whole way down. A read-only mapping opens through
// it with no cast, and nothing it hands back can be written through.
bool TableEntityBlockOpen( TableEntityBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason )
{
block.base = NULL;
block.projection = NULL;
block.bytes = 0;
int64_t used = 0;
if ( !tableentity_block_open_check( base, bytes, &used, reason ) ) { return false; }
block.base = (const uint8_t *) base;
block.projection = (const TableEntityBlock::Projection *) base;
block.bytes = used;
return true;
}

namespace {

// forward declarations, so the element column can be a constant
Expand Down Expand Up @@ -86,11 +125,19 @@ const TableBlockInfo * TableEntityBlock::Type() { return &tableentity_block_proj

// ---- the block form of table TableStat: the open path and the descriptors ----

bool TableStatBlockOpen( TableStatBlock & block, void * base, int64_t bytes, TableRefuseReason * reason )
// THE CHECK ITSELF, over a base it never writes through, because a check
// READS (docs/SPEC-TABLES.md §19.2). Both overloads of TableStatBlockOpen are
// this one body, so the producer's path and the consumer's cannot drift:
// the same clauses in the same order, and the same reason on the same
// refusal, whether the caller handed over bytes it owns or bytes it may
// only read. It answers the USED EXTENT beside the verdict, which is the
// one thing each overload has to write into its own handle.
//
// It sits in an anonymous namespace and claims no name (§11).
namespace {

bool tablestat_block_open_check( const void * base, int64_t bytes, int64_t * used_out, TableRefuseReason * reason )
{
block.base = NULL;
block.projection = NULL;
block.bytes = 0;
// a null buffer is the CALLER's defect, as an unaligned base is; a buffer
// shorter than the prologue has no prologue to read and is truncated
if ( base == NULL ) { return TableCookRefuse( reason, unaligned_base ) != NULL; }
Expand Down Expand Up @@ -120,12 +167,43 @@ bool TableStatBlockOpen( TableStatBlock & block, void * base, int64_t bytes, Tab
// the base's alignment, LAST: the only clause that reads nothing out of
// the block, and the caller's defect rather than the file's
if ( ( (uintptr_t) base % 64 ) != 0 ) { return TableCookRefuse( reason, unaligned_base ) != NULL; }
*used_out = used;
return true;
}

} // namespace

// THE PRODUCER's overload, unchanged in what it does: the check above, and
// then a handle over bytes the caller may write.
bool TableStatBlockOpen( TableStatBlock & block, void * base, int64_t bytes, TableRefuseReason * reason )
{
block.base = NULL;
block.projection = NULL;
block.bytes = 0;
int64_t used = 0;
if ( !tablestat_block_open_check( base, bytes, &used, reason ) ) { return false; }
block.base = (uint8_t *) base;
block.projection = (TableStatBlock::Projection *) base;
block.bytes = used;
return true;
}

// THE CONSUMER's overload (schema#455): the same check, and then a handle
// that carries const the whole way down. A read-only mapping opens through
// it with no cast, and nothing it hands back can be written through.
bool TableStatBlockOpen( TableStatBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason )
{
block.base = NULL;
block.projection = NULL;
block.bytes = 0;
int64_t used = 0;
if ( !tablestat_block_open_check( base, bytes, &used, reason ) ) { return false; }
block.base = (const uint8_t *) base;
block.projection = (const TableStatBlock::Projection *) base;
block.bytes = used;
return true;
}

namespace {

// forward declarations, so the element column can be a constant
Expand Down
88 changes: 88 additions & 0 deletions generated/bench/tables/cpp/BenchTableBlock.h
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,48 @@ struct TableBlockSpan
T & operator[]( int32_t i ) const { return rows[i]; }
};

// THE READ SIDE's own two views, and they are the same two with const on every
// pointer they hand out (docs/SPEC-TABLES.md §19.2). A block is memory another
// build wrote: a consumer opening bytes it received reads them and writes
// nothing, and a read-only mapping cannot be opened through a view that hands
// back a mutable row at all. The producer's pair above is untouched: Begin,
// the fill accessors and the typed base a worker indexes stay exactly what
// they were, and this pair is what the const overload of BlockOpen fills, so
// the split is the one C# already has (ref readonly, ReadOnlySpan).
template <typename T>
struct TableBlockConstRows
{
const uint8_t * base = NULL;
int32_t count = 0;
int32_t stride = 0;

struct iterator
{
const uint8_t * p;
int32_t stride;
const T & operator*() const { return *(const T *) p; }
iterator & operator++() { p += stride; return *this; }
bool operator!=( const iterator & other ) const { return p != other.p; }
};

iterator begin() const { return iterator{ base, stride }; }
iterator end() const { return iterator{ base + (ptrdiff_t) count * stride, stride }; }
int32_t size() const { return count; }
operator const T *() const { return (const T *) base; }
};

template <typename T>
struct TableBlockConstSpan
{
const T * rows = NULL;
int32_t count = 0;

const T * begin() const { return rows; }
const T * end() const { return rows + count; }
int32_t size() const { return count; }
const T & operator[]( int32_t i ) const { return rows[i]; }
};

// ---- reflection over a block (docs/SPEC-TABLES.md §8, §19.2) ----
//
// The descriptors are the mechanism, and they are what retires a hand-kept
Expand Down Expand Up @@ -356,6 +398,20 @@ struct TableEntityBlock
Projection * projection = NULL; // the projection, at offset 0
int64_t bytes = 0; // the extent in use

// THE CONSUMER's handle: this same block over bytes it may not write
// (docs/SPEC-TABLES.md §19.2). A block is memory another build wrote,
// and a consumer that received it reads it: the const overload of
// TableEntityBlockOpen fills this from a `const void *`, so a read-only
// mapping opens with no cast, and the row accessors overloaded on it
// hand back const rows. It is a MEMBER TYPE and claims no name of its
// own (§11): the producer's handle above is unchanged in every way.
struct Const
{
const uint8_t * base = NULL; // the extent's base, 64-byte aligned
const Projection * projection = NULL; // the projection, at offset 0
int64_t bytes = 0; // the extent in use
};

// this table's block descriptors (docs/SPEC-TABLES.md §8, §19.2): constant
// data, defined in the .cpp beside this header.
static const TableBlockInfo * Type();
Expand Down Expand Up @@ -463,6 +519,15 @@ inline int64_t TableEntityBlockBytes( const TableEntityBlock & block )
// build that wrote it takes the wire (§3), which this same table still has.
bool TableEntityBlockOpen( TableEntityBlock & block, void * base, int64_t bytes, TableRefuseReason * reason = NULL );

// AND ITS CONST OVERLOAD, for the consumer of foreign bytes (§19.2). A
// block arrives from another language, another process or an mmap of a
// read-only file, and a consumer that only reads should neither need a
// const_cast to open it nor receive write access to it. It is the SAME
// CHECK (one body, the same clauses in the same order, the same reason on
// the same refusal) over a base it never writes through, and it fills the
// const handle, whose row accessors hand back const rows.
bool TableEntityBlockOpen( TableEntityBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason = NULL );

// ---- the block form of table TableEntity: end ----

// ---- the block form of table TableStat (docs/SPEC-TABLES.md §19): begin ----
Expand Down Expand Up @@ -539,6 +604,20 @@ struct TableStatBlock
Projection * projection = NULL; // the projection, at offset 0
int64_t bytes = 0; // the extent in use

// THE CONSUMER's handle: this same block over bytes it may not write
// (docs/SPEC-TABLES.md §19.2). A block is memory another build wrote,
// and a consumer that received it reads it: the const overload of
// TableStatBlockOpen fills this from a `const void *`, so a read-only
// mapping opens with no cast, and the row accessors overloaded on it
// hand back const rows. It is a MEMBER TYPE and claims no name of its
// own (§11): the producer's handle above is unchanged in every way.
struct Const
{
const uint8_t * base = NULL; // the extent's base, 64-byte aligned
const Projection * projection = NULL; // the projection, at offset 0
int64_t bytes = 0; // the extent in use
};

// this table's block descriptors (docs/SPEC-TABLES.md §8, §19.2): constant
// data, defined in the .cpp beside this header.
static const TableBlockInfo * Type();
Expand Down Expand Up @@ -634,6 +713,15 @@ inline int64_t TableStatBlockBytes( const TableStatBlock & block )
// build that wrote it takes the wire (§3), which this same table still has.
bool TableStatBlockOpen( TableStatBlock & block, void * base, int64_t bytes, TableRefuseReason * reason = NULL );

// AND ITS CONST OVERLOAD, for the consumer of foreign bytes (§19.2). A
// block arrives from another language, another process or an mmap of a
// read-only file, and a consumer that only reads should neither need a
// const_cast to open it nor receive write access to it. It is the SAME
// CHECK (one body, the same clauses in the same order, the same reason on
// the same refusal) over a base it never writes through, and it fills the
// const handle, whose row accessors hand back const rows.
bool TableStatBlockOpen( TableStatBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason = NULL );

// ---- the block form of table TableStat: end ----

} // namespace benchtable
Loading
Loading