From 9951a9e6a28423cf2dedc84e380329217f9f6cba Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 7 Sep 2026 06:17:56 -0400 Subject: [PATCH 1/3] tables: the block's const read path, red first (#455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test/tables/block_const_main.cpp loads the pinned block image, holds it as a `const uint8_t *` from that moment and opens it with no cast, then walks its rows through a read-only view and compares them against the same bytes opened writable. Makefile gains `tables-block-const`, its sanitized twin and `tables-block-const-negative-control`, and `tables-block` runs all three. It is RED on this commit, which is the point of landing it first: error: no type named 'Const' in 'blockdemo::RenderFrameBlock' error: no template named 'TableBlockConstRows' error: no template named 'TableBlockConstSpan' `BlockOpen` takes `void * base` and the row views hand back mutable rows, so a consumer of foreign bytes gets write access and a const region cannot be opened at all (docs/SPEC-TABLES.md §19.2). Co-Authored-By: Claude Opus 5 --- Makefile | 45 ++++++++ test/tables/block_const_main.cpp | 173 +++++++++++++++++++++++++++++++ 2 files changed, 218 insertions(+) create mode 100644 test/tables/block_const_main.cpp diff --git a/Makefile b/Makefile index 5c8e35151..3298cec8c 100644 --- a/Makefile +++ b/Makefile @@ -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. @@ -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 diff --git a/test/tables/block_const_main.cpp b/test/tables/block_const_main.cpp new file mode 100644 index 000000000..ffbe38cfc --- /dev/null +++ b/test/tables/block_const_main.cpp @@ -0,0 +1,173 @@ +/* + THE BLOCK FORM's CONST READ PATH (docs/SPEC-TABLES.md §19.2, schema#455). + + A block is memory another build wrote, and a consumer of foreign bytes + reads it. This is the leg that holds the READ side to that: a pinned block + image is loaded into a 64-byte aligned buffer, held as a `const uint8_t *` + from that moment on, and opened WITH NO CAST. Every row it reads comes back + read-only, so the write access a producer needs never reaches a consumer + that only reads. + + It is one program with two halves, and the Makefile runs both: + + * the DEFAULT build opens the const region, walks its rows through the + const view and compares them against the same bytes opened writable, so + the two overloads agree value for value and the producer's path is + proved unchanged; + * `-DBLOCK_CONST_WRITE` adds ONE assignment through the const view, and + that build MUST NOT COMPILE. A read-only view whose writes compile is a + view in name only, which is what the negative compile control holds. + + Prints OK and exits 0 — no test framework, exit code is the verdict. +*/ + +#include "RenderBlock.h" + +#include +#include +#include +#include + +using namespace blockdemo; + +static int failures = 0; + +static void check( bool ok, const char * what ) +{ + if ( !ok ) + { + printf( "FAILED: %s\n", what ); + failures++; + } +} + +// The image, read off disk into a 64-byte aligned buffer — §19.1's base +// alignment, which BlockOpen's last clause reads. The allocation is handed +// back beside the base so the caller frees what it was given rather than the +// pointer it aligned. +static uint8_t * load_image( const char * path, int64_t * bytes, void ** allocation ) +{ + FILE * f = fopen( path, "rb" ); + if ( f == NULL ) { return NULL; } + fseek( f, 0, SEEK_END ); + const long length = ftell( f ); + fseek( f, 0, SEEK_SET ); + if ( length <= 0 ) { fclose( f ); return NULL; } + void * raw = malloc( (size_t) length + 63 ); + if ( raw == NULL ) { fclose( f ); return NULL; } + uint8_t * base = (uint8_t *) ( ( (uintptr_t) raw + 63 ) & ~(uintptr_t) 63 ); + const size_t read = fread( base, 1, (size_t) length, f ); + fclose( f ); + if ( read != (size_t) length ) { free( raw ); return NULL; } + *bytes = (int64_t) length; + *allocation = raw; + return base; +} + +int main( int argc, char ** argv ) +{ + const char * path = argc > 1 ? argv[1] : "testdata/wire/tables/block_render.bin"; + + int64_t bytes = 0; + void * allocation = NULL; + uint8_t * loaded = load_image( path, &bytes, &allocation ); + if ( loaded == NULL ) + { + printf( "FAILED: could not read the block image %s\n", path ); + return 1; + } + + // FROM HERE THE BYTES ARE CONST, and nothing below casts that away. This + // is the consumer an mmap of a read-only file hands its bytes to (#455). + const uint8_t * region = loaded; + + RenderFrameBlock::Const block; + TableRefuseReason reason = ok; + check( RenderFrameBlockOpen( block, region, bytes, &reason ), + "a const region opens with no cast (docs/SPEC-TABLES.md §19.2)" ); + check( reason == ok, "a matched open writes no reason" ); + check( block.base == region, "the const handle points at the bytes it was given" ); + check( block.projection != NULL, "the const handle carries the projection" ); + check( block.bytes > 0 && block.bytes <= bytes, "the used extent lies inside the caller's bytes" ); + + // THE ROWS, READ-ONLY. The iteration is §19.2's — at the pitch the + // instance gives, never spelled at the call site — and what it yields is a + // reference a consumer cannot write through. + int32_t iterated = 0; + uint32_t checksum = 0; + for ( const RenderShip & ship : RenderFrameShips( block ) ) + { + iterated++; + checksum += ship.object_id; + } + TableBlockConstRows rows = RenderFrameShips( block ); + check( iterated == rows.size(), "the const iteration visits every row of the array" ); + check( rows.size() == (int32_t) block.projection->ships.count, + "and the count it walks is the INSTANCE's, never a constant of this build (§19.2)" ); + + TableBlockConstSpan span = RenderFrameShipsSpan( block ); + check( span.size() == rows.size(), "the contiguous const view carries the same count" ); + uint32_t span_checksum = 0; + for ( int32_t i = 0; i < span.size(); i++ ) { span_checksum += span[i].object_id; } + check( span_checksum == checksum, "and the same rows: the pitch IS sizeof (§2.7)" ); + + const RenderShip * base_pointer = span.begin(); + check( base_pointer == (const RenderShip *) ( region + block.projection->ships.offset_of ), + "the const span begins at the array's own offset_of" ); + + // THE PRODUCER'S PATH IS UNCHANGED, and that is half the claim: the same + // bytes, opened through the mutable overload, are the same rows. A copy is + // taken because a mutable open is a mutable base, and the const region + // above must not be reachable from it. + void * mutable_allocation = NULL; + int64_t mutable_bytes = 0; + uint8_t * writable = load_image( path, &mutable_bytes, &mutable_allocation ); + if ( writable == NULL ) + { + printf( "FAILED: could not read the block image a second time\n" ); + free( allocation ); + return 1; + } + RenderFrameBlock mutable_block; + check( RenderFrameBlockOpen( mutable_block, writable, mutable_bytes ), + "the mutable overload still opens the same bytes" ); + TableBlockRows mutable_rows = RenderFrameShips( mutable_block ); + check( mutable_rows.size() == rows.size(), "both overloads read the same count" ); + int mismatches = 0; + for ( int32_t i = 0; i < rows.size(); i++ ) + { + if ( memcmp( &rows[i], &mutable_rows[i], sizeof( RenderShip ) ) != 0 ) { mismatches++; } + } + check( mismatches == 0, "and the same row bytes, value for value" ); + + // A NULL base and a short buffer answer on the const overload exactly as + // they do on the mutable one: one check, two entry points (§19.2). + RenderFrameBlock::Const refused; + reason = ok; + check( !RenderFrameBlockOpen( refused, (const void *) NULL, bytes, &reason ), "a null const base refuses" ); + check( reason == unaligned_base, "and names the caller's own defect" ); + reason = ok; + check( !RenderFrameBlockOpen( refused, region, 8, &reason ), "a const buffer shorter than the projection refuses" ); + check( reason == truncated, "and names it truncated" ); + reason = ok; + check( !RenderFrameBlockOpen( refused, region + 1, bytes - 1, &reason ), "an unaligned const base refuses" ); + check( reason == unaligned_base, "and names the caller's own defect, last (§19.2)" ); + +#if defined( BLOCK_CONST_WRITE ) + // THE NEGATIVE COMPILE CONTROL. One assignment through the const view, and + // this build must not compile: a read-only view whose writes compile is a + // view in name only. + RenderFrameShipsSpan( block )[0].object_id = 1; +#endif + + free( mutable_allocation ); + free( allocation ); + + if ( failures > 0 ) + { + printf( "%d failed\n", failures ); + return 1; + } + printf( "OK: a const block region opens with no cast, and its rows read back read-only (docs/SPEC-TABLES.md §19.2)\n" ); + return 0; +} From 74fd04c01de8c45f64363d001174f05e63bc2c26 Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 7 Sep 2026 06:23:11 -0400 Subject: [PATCH 2/3] tables: the block opens a const region and reads const rows (#455) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit internal/codegen/cpptable/block.go gains three things, and the producer's path gains nothing: * TableBlockConstRows and TableBlockConstSpan in the shared runtime, the same two views with const on every pointer they hand out. Both are runtime names, so no per-table spelling is claimed (SPEC-TABLES §11). * Block::Const, a member type of the handle that already exists, so it claims no name of its own. It carries the same three facts with const on both pointers. * bool BlockOpen( Block::Const &, const void *, int64_t, TableRefuseReason * = NULL ), and the row accessors overloaded on the const handle. The CHECK is now one body per table, in an anonymous namespace in the .cpp, answering the used extent beside the verdict: both overloads run the same clauses in the same order and name the same reason on the same refusal, so the producer's path and the consumer's cannot drift. Begin, the fill accessors and the typed base a worker indexes are untouched. docs/SPEC-TABLES.md §19.2 states the overload, the const handle and the two const views, and §19.5 states the gate and its control. Goldens moved, and only by the added surface: generated/bench/tables/cpp/BenchTableBlock.h +88 generated/bench/tables/cpp/BenchTableBlock.cpp +94 -8 testdata/golden/wide/cpp/CaptionBlock.h +65 testdata/golden/wide/cpp/CaptionBlock.cpp +47 -4 No wire pin moved: testdata/wire/ is byte-identical, because a C++ overload changes no layout fact the BUILD VERSION is taken over (SPEC-TABLES §20.1). The gate is green and its control is red on the write: OK: a const block region opens with no cast, and its rows read back read-only (docs/SPEC-TABLES.md §19.2) error: cannot assign to return value because function 'operator[]' returns a const value RenderFrameShipsSpan( block )[0].object_id = 1; note: function 'operator[]' which returns const-qualified type 'const blockdemo::RenderShip &' declared here Co-Authored-By: Claude Opus 5 --- Makefile | 2 +- docs/SPEC-TABLES.md | 31 ++++ .../bench/tables/cpp/BenchTableBlock.cpp | 94 +++++++++++- generated/bench/tables/cpp/BenchTableBlock.h | 88 +++++++++++ internal/codegen/cpptable/block.go | 140 +++++++++++++++++- test/tables/block_const_main.cpp | 26 +++- testdata/golden/wide/cpp/CaptionBlock.cpp | 47 +++++- testdata/golden/wide/cpp/CaptionBlock.h | 65 ++++++++ 8 files changed, 472 insertions(+), 21 deletions(-) diff --git a/Makefile b/Makefile index 3298cec8c..03a5f5a85 100644 --- a/Makefile +++ b/Makefile @@ -726,7 +726,7 @@ tables-block-const: build/schema_test_block_const build/schema_test_block_const_ # 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 +# 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 diff --git a/docs/SPEC-TABLES.md b/docs/SPEC-TABLES.md index e8f113f4a..6bca5db72 100644 --- a/docs/SPEC-TABLES.md +++ b/docs/SPEC-TABLES.md @@ -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 BlockOpen( Block::Const & block, const void * base, + int64_t bytes, TableRefuseReason * reason = NULL )` sits beside the mutable + one. `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` and `TableBlockConstSpan`, 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`, 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 @@ -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 diff --git a/generated/bench/tables/cpp/BenchTableBlock.cpp b/generated/bench/tables/cpp/BenchTableBlock.cpp index 925d8e3e4..0f331ad5a 100644 --- a/generated/bench/tables/cpp/BenchTableBlock.cpp +++ b/generated/bench/tables/cpp/BenchTableBlock.cpp @@ -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; } @@ -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 @@ -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; } @@ -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 diff --git a/generated/bench/tables/cpp/BenchTableBlock.h b/generated/bench/tables/cpp/BenchTableBlock.h index f70b1c64c..ed678a3f6 100644 --- a/generated/bench/tables/cpp/BenchTableBlock.h +++ b/generated/bench/tables/cpp/BenchTableBlock.h @@ -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 +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 +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 @@ -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(); @@ -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 ---- @@ -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(); @@ -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 diff --git a/internal/codegen/cpptable/block.go b/internal/codegen/cpptable/block.go index 8a0e56fde..a48fdeb40 100644 --- a/internal/codegen/cpptable/block.go +++ b/internal/codegen/cpptable/block.go @@ -154,6 +154,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 +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 +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 @@ -448,6 +490,18 @@ func (g *tableGen) emitBlockSurface(bl *ir.BlockLayout) { g.pf(" uint8_t * base = NULL; // the extent's base, 64-byte aligned\n") g.pf(" Projection * projection = NULL; // the projection, at offset 0\n") g.pf(" int64_t bytes = 0; // the extent in use\n") + g.pf("\n // THE CONSUMER's handle: this same block over bytes it may not write\n") + g.pf(" // (docs/SPEC-TABLES.md §19.2). A block is memory another build wrote,\n") + g.pf(" // and a consumer that received it reads it: the const overload of\n") + g.pf(" // %sBlockOpen fills this from a `const void *`, so a read-only\n", name) + g.pf(" // mapping opens with no cast, and the row accessors overloaded on it\n") + g.pf(" // hand back const rows. It is a MEMBER TYPE and claims no name of its\n") + g.pf(" // own (§11): the producer's handle above is unchanged in every way.\n") + g.pf(" struct Const\n {\n") + g.pf(" const uint8_t * base = NULL; // the extent's base, 64-byte aligned\n") + g.pf(" const Projection * projection = NULL; // the projection, at offset 0\n") + g.pf(" int64_t bytes = 0; // the extent in use\n") + g.pf(" };\n") for _, a := range bl.Arrays { field := ir.GoExportName(a.Field.Name) g.pf("\n // %s: the constants this build asserts against. A consumer INDEXES with\n", a.Field.Name) @@ -487,6 +541,17 @@ func (g *tableGen) emitBlockSurface(bl *ir.BlockLayout) { g.pf("// build that wrote it takes the wire (§3), which this same table still has.\n") g.pf("bool %sBlockOpen( %sBlock & block, void * base, int64_t bytes, TableRefuseReason * reason = NULL );\n\n", name, name) + g.pf("// AND ITS CONST OVERLOAD, for the consumer of foreign bytes (§19.2). A\n") + g.pf("// block arrives from another language, another process or an mmap of a\n") + g.pf("// read-only file, and a consumer that only reads should neither need a\n") + g.pf("// const_cast to open it nor receive write access to it. It is the SAME\n") + g.pf("// CHECK (one body, the same clauses in the same order, the same reason on\n") + g.pf("// the same refusal) over a base it never writes through, and it fills the\n") + g.pf("// const handle, whose row accessors hand back const rows.\n") + g.pf("bool %sBlockOpen( %sBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason = NULL );\n\n", name, name) + + g.emitBlockConstReadPath(bl) + g.pf("// ---- the block form of table %s: end ----\n\n", name) } @@ -654,6 +719,45 @@ func (g *tableGen) emitBlockFillPath(bl *ir.BlockLayout) { g.pf("// ---- block fill path: end ----\n\n") } +// emitBlockConstReadPath emits the row accessors overloaded on the CONST +// handle (docs/SPEC-TABLES.md §19.2, schema#455): the consumer's half of the +// surface the fill path above gives the producer. +// +// It sits OUTSIDE the fill-path markers on purpose: nothing here is reached at +// fill time, and the conformance refuser reads what lies between them. Each +// accessor is the same one add the producer's is, typed const, so a consumer +// of foreign bytes iterates at the pitch the instance gives and writes +// nothing. The names are the producer's own, an overload rather than a second +// spelling, so a call site written against §19.2 reads the same either way. +func (g *tableGen) emitBlockConstReadPath(bl *ir.BlockLayout) { + name := bl.Table.Name + if len(bl.Arrays) == 0 { + return + } + g.pf("// ---- the block form's CONST read path (docs/SPEC-TABLES.md §19.2) ----\n\n") + for _, a := range bl.Arrays { + field := ir.GoExportName(a.Field.Name) + g.pf("// %s, over a block a consumer RECEIVED: the same one add, the same pitch\n", a.Field.Name) + g.pf("// out of the instance, and a row reference the caller cannot write\n") + g.pf("// through. A producer holds the mutable overload above; a consumer of\n") + g.pf("// foreign bytes holds this one.\n") + g.pf("inline TableBlockConstRows<%s> %s%s( const %sBlock::Const & block )\n{\n", a.ElemName, name, field, name) + g.pf(" TableBlockConstRows<%s> rows;\n", a.ElemName) + g.pf(" rows.base = block.base + block.projection->%s.offset_of;\n", a.Field.Name) + g.pf(" rows.count = (int32_t) block.projection->%s.count;\n", a.Field.Name) + g.pf(" rows.stride = (int32_t) block.projection->%s.stride;\n", a.Field.Name) + g.pf(" return rows;\n}\n\n") + + g.pf("// %s, as a CONTIGUOUS const view, available because the pitch IS sizeof\n", a.Field.Name) + g.pf("// (§2.7), which is how a consumer's fast path is actually written.\n") + g.pf("inline TableBlockConstSpan<%s> %s%sSpan( const %sBlock::Const & block )\n{\n", a.ElemName, name, field, name) + g.pf(" TableBlockConstSpan<%s> span;\n", a.ElemName) + g.pf(" span.rows = (const %s *) ( block.base + block.projection->%s.offset_of );\n", a.ElemName, a.Field.Name) + g.pf(" span.count = (int32_t) block.projection->%s.count;\n", a.Field.Name) + g.pf(" return span;\n}\n\n") + } +} + // blockStartAlign is where one out-of-line array begins: aligned to // max( 64, alignof( element ) ) (docs/SPEC-TABLES.md §19.1). func blockStartAlign(a ir.BlockArray) int64 { @@ -674,8 +778,18 @@ func (g *tableGen) emitBlockDefinitions(bl *ir.BlockLayout) { func (g *tableGen) emitBlockOpenBody(bl *ir.BlockLayout) { name := bl.Table.Name - g.pf("bool %sBlockOpen( %sBlock & block, void * base, int64_t bytes, TableRefuseReason * reason )\n{\n", name, name) - g.pf(" block.base = NULL;\n block.projection = NULL;\n block.bytes = 0;\n") + check := strings.ToLower(name) + "_block_open_check" + g.pf("// THE CHECK ITSELF, over a base it never writes through, because a check\n") + g.pf("// READS (docs/SPEC-TABLES.md §19.2). Both overloads of %sBlockOpen are\n", name) + g.pf("// this one body, so the producer's path and the consumer's cannot drift:\n") + g.pf("// the same clauses in the same order, and the same reason on the same\n") + g.pf("// refusal, whether the caller handed over bytes it owns or bytes it may\n") + g.pf("// only read. It answers the USED EXTENT beside the verdict, which is the\n") + g.pf("// one thing each overload has to write into its own handle.\n") + g.pf("//\n") + g.pf("// It sits in an anonymous namespace and claims no name (§11).\n") + g.pf("namespace {\n\n") + g.pf("bool %s( const void * base, int64_t bytes, int64_t * used_out, TableRefuseReason * reason )\n{\n", check) g.pf(" // a null buffer is the CALLER's defect, as an unaligned base is; a buffer\n") g.pf(" // shorter than the prologue has no prologue to read and is truncated\n") g.pf(" if ( base == NULL ) { return TableCookRefuse( reason, unaligned_base ) != NULL; }\n") @@ -730,10 +844,32 @@ func (g *tableGen) emitBlockOpenBody(bl *ir.BlockLayout) { g.pf(" // the base's alignment, LAST: the only clause that reads nothing out of\n") g.pf(" // the block, and the caller's defect rather than the file's\n") g.pf(" if ( ( (uintptr_t) base %% %d ) != 0 ) { return TableCookRefuse( reason, unaligned_base ) != NULL; }\n", ir.BlockAlign) + g.pf(" *used_out = used;\n") + g.pf(" return true;\n}\n\n") + g.pf("} // namespace\n\n") + + g.pf("// THE PRODUCER's overload, unchanged in what it does: the check above, and\n") + g.pf("// then a handle over bytes the caller may write.\n") + g.pf("bool %sBlockOpen( %sBlock & block, void * base, int64_t bytes, TableRefuseReason * reason )\n{\n", name, name) + g.pf(" block.base = NULL;\n block.projection = NULL;\n block.bytes = 0;\n") + g.pf(" int64_t used = 0;\n") + g.pf(" if ( !%s( base, bytes, &used, reason ) ) { return false; }\n", check) g.pf(" block.base = (uint8_t *) base;\n") g.pf(" block.projection = (%sBlock::Projection *) base;\n", name) g.pf(" block.bytes = used;\n") g.pf(" return true;\n}\n\n") + + g.pf("// THE CONSUMER's overload (schema#455): the same check, and then a handle\n") + g.pf("// that carries const the whole way down. A read-only mapping opens through\n") + g.pf("// it with no cast, and nothing it hands back can be written through.\n") + g.pf("bool %sBlockOpen( %sBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason )\n{\n", name, name) + g.pf(" block.base = NULL;\n block.projection = NULL;\n block.bytes = 0;\n") + g.pf(" int64_t used = 0;\n") + g.pf(" if ( !%s( base, bytes, &used, reason ) ) { return false; }\n", check) + g.pf(" block.base = (const uint8_t *) base;\n") + g.pf(" block.projection = (const %sBlock::Projection *) base;\n", name) + g.pf(" block.bytes = used;\n") + g.pf(" return true;\n}\n\n") } // emitBlockDescriptors emits one table's block reflection: the projection diff --git a/test/tables/block_const_main.cpp b/test/tables/block_const_main.cpp index ffbe38cfc..c71ddc3e3 100644 --- a/test/tables/block_const_main.cpp +++ b/test/tables/block_const_main.cpp @@ -18,7 +18,7 @@ that build MUST NOT COMPILE. A read-only view whose writes compile is a view in name only, which is what the negative compile control holds. - Prints OK and exits 0 — no test framework, exit code is the verdict. + Prints OK and exits 0: no test framework, exit code is the verdict. */ #include "RenderBlock.h" @@ -41,8 +41,8 @@ static void check( bool ok, const char * what ) } } -// The image, read off disk into a 64-byte aligned buffer — §19.1's base -// alignment, which BlockOpen's last clause reads. The allocation is handed +// The image, read off disk into a 64-byte aligned buffer, which is §19.1's +// base alignment and BlockOpen's last clause. The allocation is handed // back beside the base so the caller frees what it was given rather than the // pointer it aligned. static uint8_t * load_image( const char * path, int64_t * bytes, void ** allocation ) @@ -90,8 +90,8 @@ int main( int argc, char ** argv ) check( block.projection != NULL, "the const handle carries the projection" ); check( block.bytes > 0 && block.bytes <= bytes, "the used extent lies inside the caller's bytes" ); - // THE ROWS, READ-ONLY. The iteration is §19.2's — at the pitch the - // instance gives, never spelled at the call site — and what it yields is a + // THE ROWS, READ-ONLY. The iteration is §19.2's, at the pitch the instance + // gives and never spelled at the call site, and what it yields is a // reference a consumer cannot write through. int32_t iterated = 0; uint32_t checksum = 0; @@ -149,9 +149,23 @@ int main( int argc, char ** argv ) reason = ok; check( !RenderFrameBlockOpen( refused, region, 8, &reason ), "a const buffer shorter than the projection refuses" ); check( reason == truncated, "and names it truncated" ); + // the base's alignment is the LAST clause (§19.2), so the image is moved + // whole to a base eight bytes off the sixty-four: every clause before it + // reads exactly what it read above, and only the alignment is wrong. + void * offset_allocation = malloc( (size_t) bytes + 128 ); + if ( offset_allocation == NULL ) + { + printf( "FAILED: could not allocate the unaligned image\n" ); + free( allocation ); + return 1; + } + uint8_t * offset_base = (uint8_t *) ( ( (uintptr_t) offset_allocation + 63 ) & ~(uintptr_t) 63 ) + 8; + memcpy( offset_base, region, (size_t) bytes ); reason = ok; - check( !RenderFrameBlockOpen( refused, region + 1, bytes - 1, &reason ), "an unaligned const base refuses" ); + check( !RenderFrameBlockOpen( refused, (const uint8_t *) offset_base, bytes, &reason ), + "an unaligned const base refuses" ); check( reason == unaligned_base, "and names the caller's own defect, last (§19.2)" ); + free( offset_allocation ); #if defined( BLOCK_CONST_WRITE ) // THE NEGATIVE COMPILE CONTROL. One assignment through the const view, and diff --git a/testdata/golden/wide/cpp/CaptionBlock.cpp b/testdata/golden/wide/cpp/CaptionBlock.cpp index d4ac28b6b..1f50abfbf 100644 --- a/testdata/golden/wide/cpp/CaptionBlock.cpp +++ b/testdata/golden/wide/cpp/CaptionBlock.cpp @@ -15,11 +15,19 @@ namespace wide { // ---- the block form of table Stamp: the open path and the descriptors ---- -bool StampBlockOpen( StampBlock & 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 StampBlockOpen 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 stamp_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; } @@ -49,12 +57,43 @@ bool StampBlockOpen( StampBlock & block, void * base, int64_t bytes, TableRefuse // 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 StampBlockOpen( StampBlock & block, void * base, int64_t bytes, TableRefuseReason * reason ) +{ + block.base = NULL; + block.projection = NULL; + block.bytes = 0; + int64_t used = 0; + if ( !stamp_block_open_check( base, bytes, &used, reason ) ) { return false; } block.base = (uint8_t *) base; block.projection = (StampBlock::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 StampBlockOpen( StampBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason ) +{ + block.base = NULL; + block.projection = NULL; + block.bytes = 0; + int64_t used = 0; + if ( !stamp_block_open_check( base, bytes, &used, reason ) ) { return false; } + block.base = (const uint8_t *) base; + block.projection = (const StampBlock::Projection *) base; + block.bytes = used; + return true; +} + namespace { // forward declarations, so the element column can be a constant diff --git a/testdata/golden/wide/cpp/CaptionBlock.h b/testdata/golden/wide/cpp/CaptionBlock.h index f13a140a8..41f331912 100644 --- a/testdata/golden/wide/cpp/CaptionBlock.h +++ b/testdata/golden/wide/cpp/CaptionBlock.h @@ -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 +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 +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 @@ -345,6 +387,20 @@ struct StampBlock 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 + // StampBlockOpen 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(); @@ -440,6 +496,15 @@ inline int64_t StampBlockBytes( const StampBlock & block ) // build that wrote it takes the wire (§3), which this same table still has. bool StampBlockOpen( StampBlock & 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 StampBlockOpen( StampBlock::Const & block, const void * base, int64_t bytes, TableRefuseReason * reason = NULL ); + // ---- the block form of table Stamp: end ---- } // namespace wide From 0ded1ec9d9956d6d3bca55b571a0a7d1644a77a3 Mon Sep 17 00:00:00 2001 From: Rowan Date: Mon, 7 Sep 2026 07:03:20 -0400 Subject: [PATCH 3/3] tables: the const-view compile control covers the rows view's typed base and iterator too (#455) Co-Authored-By: Claude Fable 5.1 --- test/tables/block_const_main.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/tables/block_const_main.cpp b/test/tables/block_const_main.cpp index c71ddc3e3..04c2603e5 100644 --- a/test/tables/block_const_main.cpp +++ b/test/tables/block_const_main.cpp @@ -172,6 +172,10 @@ int main( int argc, char ** argv ) // this build must not compile: a read-only view whose writes compile is a // view in name only. RenderFrameShipsSpan( block )[0].object_id = 1; + // and the rows view's two other doors, the typed base and the iterator, + // each held read-only by the same build + RenderFrameShips( block )[0].object_id = 1; + for ( RenderShip & ship : RenderFrameShips( block ) ) { ship.object_id = 2; } #endif free( mutable_allocation );