@@ -79,6 +79,13 @@ function queryIndexKeyExpr(fieldType: FieldType, expr: string): string {
7979 : `keccak256(abi.encode(${ expr } ))` ;
8080}
8181
82+ function chunkArray < T > ( items : T [ ] , size : number ) : T [ ] [ ] {
83+ if ( size <= 0 ) return [ items . slice ( ) ] ;
84+ const out : T [ ] [ ] = [ ] ;
85+ for ( let i = 0 ; i < items . length ; i += size ) out . push ( items . slice ( i , i + size ) ) ;
86+ return out ;
87+ }
88+
8289class W {
8390 private lines : string [ ] = [ ] ;
8491 private indent = 0 ;
@@ -130,10 +137,6 @@ function bytes32FromSha256(schemaHash: string): string {
130137 return `0x${ m [ 1 ] } ` ;
131138}
132139
133- function recordHashFnName ( collectionName : string ) : string {
134- return `_hashRecord${ collectionName } ` ;
135- }
136-
137140export function generateAppSolidity ( schema : ThsSchema , options : GenerateAppSolidityOptions = { } ) : { path : string ; contents : string } {
138141 const w = new W ( ) ;
139142
@@ -257,7 +260,7 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
257260 w . line ( '(bool ok, bytes memory res) = address(this).delegatecall(calls[i]);' ) ;
258261 w . block ( 'if (!ok)' , ( ) => {
259262 // bubble up revert data (best-effort)
260- w . block ( 'assembly' , ( ) => {
263+ w . block ( 'assembly ("memory-safe") ' , ( ) => {
261264 w . line ( 'revert(add(res, 32), mload(res))' ) ;
262265 } ) ;
263266 } ) ;
@@ -359,14 +362,6 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
359362 } ) ;
360363 w . line ( ) ;
361364
362- // Record hashing helper (used for event integrity without stack-too-deep risk).
363- // Note: this uses ABI encoding of the full record tuple; callers can recompute
364- // from decoded record values.
365- w . block ( `function ${ recordHashFnName ( C ) } (${ record } memory r) internal pure returns (bytes32)` , ( ) => {
366- w . line ( `return keccak256(abi.encode(COLLECTION_ID_${ C } , r));` ) ;
367- } ) ;
368- w . line ( ) ;
369-
370365 w . block ( `function _init${ record } (${ record } storage r, uint256 id) internal` , ( ) => {
371366 w . line ( 'r.id = id;' ) ;
372367 w . line ( 'r.createdAt = block.timestamp;' ) ;
@@ -380,17 +375,45 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
380375 } ) ;
381376 w . line ( ) ;
382377
378+ const createFieldChunks = chunkArray ( c . fields , 4 ) ;
379+ for ( let i = 0 ; i < createFieldChunks . length ; i ++ ) {
380+ const chunk = createFieldChunks [ i ] ! ;
381+ w . block ( `function _applyCreate${ C } Fields_${ i } (${ record } storage r, ${ createInputStruct } calldata input) internal` , ( ) => {
382+ for ( const f of chunk ) {
383+ w . line ( `r.${ f . name } = input.${ f . name } ;` ) ;
384+ }
385+ } ) ;
386+ w . line ( ) ;
387+ }
388+
383389 w . block ( `function _applyCreate${ C } Fields(${ record } storage r, ${ createInputStruct } calldata input) internal` , ( ) => {
384- for ( const f of c . fields ) {
385- w . line ( `r. ${ f . name } = input. ${ f . name } ;` ) ;
390+ for ( let i = 0 ; i < createFieldChunks . length ; i ++ ) {
391+ w . line ( `_applyCreate ${ C } Fields_ ${ i } (r, input) ;` ) ;
386392 }
387393 } ) ;
388394 w . line ( ) ;
389395
396+ const recordHashParts = [
397+ 'COLLECTION_ID_' + C ,
398+ 'r.id' ,
399+ 'r.createdAt' ,
400+ 'r.createdBy' ,
401+ 'r.owner' ,
402+ 'r.updatedAt' ,
403+ 'r.updatedBy' ,
404+ 'r.isDeleted' ,
405+ 'r.deletedAt' ,
406+ 'r.version' ,
407+ ...c . fields . map ( ( f ) => `r.${ f . name } ` )
408+ ] ;
409+ w . block ( `function _recordHash${ C } (${ record } storage r) internal view returns (bytes32)` , ( ) => {
410+ w . line ( `return keccak256(abi.encode(${ recordHashParts . join ( ', ' ) } ));` ) ;
411+ } ) ;
412+ w . line ( ) ;
413+
390414 w . block ( `function _emitCreated${ C } (uint256 id) internal` , ( ) => {
391- w . line ( `${ record } memory m = ${ cVar } Records[id];` ) ;
392- w . line ( `bytes32 dataHash = ${ recordHashFnName ( C ) } (m);` ) ;
393- w . line ( `emit RecordCreated(COLLECTION_ID_${ C } , id, _msgSender(), block.timestamp, dataHash);` ) ;
415+ w . line ( `${ record } storage r = ${ cVar } Records[id];` ) ;
416+ w . line ( `emit RecordCreated(COLLECTION_ID_${ C } , id, _msgSender(), block.timestamp, _recordHash${ C } (r));` ) ;
394417 } ) ;
395418 w . line ( ) ;
396419
@@ -484,109 +507,6 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
484507 ) ;
485508 w . line ( ) ;
486509
487- // Reverse reference accessor(s)
488- if ( onChainIndexing && c . relations ) {
489- for ( const rel of c . relations . filter ( ( r ) => r . reverseIndex ) ) {
490- w . block (
491- `function listByRef${ C } _${ rel . field } (uint256 refId, uint256 offset, uint256 limit) external view returns (uint256[] memory)` ,
492- ( ) => {
493- w . line ( 'if (limit > MAX_LIST_LIMIT) revert InvalidLimit();' ) ;
494- w . line ( `uint256[] storage bucket = refIndex_${ C } _${ rel . field } [refId];` ) ;
495- w . line ( 'if (offset >= bucket.length) {' ) ;
496- w . line ( ' return new uint256[](0);' ) ;
497- w . line ( '}' ) ;
498- w . line ( 'uint256 end = offset + limit;' ) ;
499- w . line ( 'if (end > bucket.length) end = bucket.length;' ) ;
500- w . line ( 'uint256 outLen = end - offset;' ) ;
501- w . line ( 'uint256[] memory out = new uint256[](outLen);' ) ;
502- w . block ( 'for (uint256 i = 0; i < outLen; i++)' , ( ) => {
503- w . line ( 'out[i] = bucket[offset + i];' ) ;
504- } ) ;
505- w . line ( 'return out;' ) ;
506- }
507- ) ;
508- w . line ( ) ;
509- }
510- }
511- if ( onChainIndexing ) {
512- for ( const idx of c . indexes . index ) {
513- w . line ( `mapping(bytes32 => uint256[]) private index_${ C } _${ idx . field } ;` ) ;
514- }
515- if ( c . indexes . index . length > 0 ) w . line ( ) ;
516- }
517-
518- // Reverse reference indexes (append-only)
519- if ( onChainIndexing ) {
520- const rels = relationIndexes . filter ( ( r ) => r . from . name === C && r . rel . reverseIndex ) ;
521- for ( const r of rels ) {
522- w . line ( `mapping(uint256 => uint256[]) private refIndex_${ C } _${ r . rel . field } ;` ) ;
523- }
524- if ( rels . length > 0 ) w . line ( ) ;
525- }
526-
527- // exists / getCount
528- w . block ( `function exists${ C } (uint256 id) public view returns (bool)` , ( ) => {
529- w . line ( `${ record } storage r = ${ cVar } Records[id];` ) ;
530- w . line ( 'if (r.createdBy == address(0)) return false;' ) ;
531- w . line ( 'if (r.isDeleted) return false;' ) ;
532- w . line ( 'return true;' ) ;
533- } ) ;
534- w . line ( ) ;
535-
536- w . block ( `function getCount${ C } (bool includeDeleted) external view returns (uint256)` , ( ) => {
537- w . line ( 'if (includeDeleted) {' ) ;
538- w . line ( ` return nextId${ C } - 1;` ) ;
539- w . line ( '}' ) ;
540- w . line ( `return activeCount${ C } ;` ) ;
541- } ) ;
542- w . line ( ) ;
543-
544- // get
545- w . block ( `function get${ C } (uint256 id, bool includeDeleted) public view returns (${ record } memory)` , ( ) => {
546- w . line ( `${ record } storage r = ${ cVar } Records[id];` ) ;
547- w . line ( 'if (r.createdBy == address(0)) revert RecordNotFound();' ) ;
548- w . line ( 'if (!includeDeleted && r.isDeleted) revert RecordIsDeleted();' ) ;
549- w . line ( 'return r;' ) ;
550- } ) ;
551- w . line ( ) ;
552-
553- w . block ( `function get${ C } (uint256 id) external view returns (${ record } memory)` , ( ) => {
554- w . line ( `return get${ C } (id, false);` ) ;
555- } ) ;
556- w . line ( ) ;
557-
558- // listIds
559- w . block (
560- `function listIds${ C } (uint256 cursorIdExclusive, uint256 limit, bool includeDeleted) external view returns (uint256[] memory)` ,
561- ( ) => {
562- w . line ( 'if (limit > MAX_LIST_LIMIT) revert InvalidLimit();' ) ;
563- w . line ( `uint256 cursor = cursorIdExclusive;` ) ;
564- w . line ( `uint256 nextId = nextId${ C } ;` ) ;
565- w . line ( 'if (cursor == 0 || cursor > nextId) {' ) ;
566- w . line ( ' cursor = nextId;' ) ;
567- w . line ( '}' ) ;
568- w . line ( 'uint256[] memory tmp = new uint256[](limit);' ) ;
569- w . line ( 'uint256 found = 0;' ) ;
570- w . line ( 'uint256 steps = 0;' ) ;
571- w . line ( 'uint256 id = cursor;' ) ;
572- w . block ( 'while (id > 1 && found < limit && steps < MAX_SCAN_STEPS)' , ( ) => {
573- w . line ( 'id--;' ) ;
574- w . line ( 'steps++;' ) ;
575- w . line ( `${ record } storage r = ${ cVar } Records[id];` ) ;
576- w . line ( 'if (r.createdBy == address(0)) { continue; }' ) ;
577- w . line ( 'if (!includeDeleted && r.isDeleted) { continue; }' ) ;
578- w . line ( 'tmp[found] = id;' ) ;
579- w . line ( 'found++;' ) ;
580- } ) ;
581- w . line ( 'uint256[] memory out = new uint256[](found);' ) ;
582- w . block ( 'for (uint256 i = 0; i < found; i++)' , ( ) => {
583- w . line ( 'out[i] = tmp[i];' ) ;
584- } ) ;
585- w . line ( 'return out;' ) ;
586- }
587- ) ;
588- w . line ( ) ;
589-
590510 // Reverse reference accessor(s)
591511 if ( onChainIndexing && c . relations ) {
592512 for ( const rel of c . relations . filter ( ( r ) => r . reverseIndex ) ) {
@@ -805,9 +725,7 @@ export function generateAppSolidity(schema: ThsSchema, options: GenerateAppSolid
805725 w . line ( 'r.updatedAt = block.timestamp;' ) ;
806726 w . line ( 'r.updatedBy = _msgSender();' ) ;
807727 w . line ( 'r.version += 1;' ) ;
808- w . line ( `${ record } memory m = r;` ) ;
809- w . line ( `bytes32 changedFieldsHash = ${ recordHashFnName ( C ) } (m);` ) ;
810- w . line ( `emit RecordUpdated(COLLECTION_ID_${ C } , id, _msgSender(), block.timestamp, changedFieldsHash);` ) ;
728+ w . line ( `emit RecordUpdated(COLLECTION_ID_${ C } , id, _msgSender(), block.timestamp, _recordHash${ C } (r));` ) ;
811729 } ) ;
812730 w . line ( ) ;
813731 }
0 commit comments