@@ -68,10 +68,35 @@ export interface GeaIrStore {
6868 runtimeBase : 'compiled' | 'lean'
6969 fields : GeaIrStoreField [ ]
7070 methods ?: GeaIrStoreMethod [ ]
71+ getters ?: GeaIrStoreGetter [ ]
7172 constants ?: GeaIrConstant [ ]
7273 sourceSpan ?: GeaIrSourceSpan
7374}
7475
76+ // A getter (`get x() { ... }`) on a Store. Surfaced into the IR so the embedded
77+ // target can back a reactive `{this.x.map(...)}` list with a derived array:
78+ // `deps` are the reactive fields whose change should recompute the list, and
79+ // `elementTypeName`/`shape` describe the row element type. (Getters are NOT
80+ // regular fields — geatsc must CALL the getter, not read stored state.)
81+ export interface GeaIrStoreGetter {
82+ name : string
83+ // True when the getter yields an array (array literal/spread, an
84+ // array-producing call like `.filter`/`.map`/`.slice`, or a `T[]`/`Array<T>`
85+ // return annotation). Only array getters can back a reactive list.
86+ returnsArray : boolean
87+ // The named element type from the return annotation (`get x(): Tile[]` →
88+ // 'Tile'); geatsc reuses its declared `__gea_type_<Name>` struct as the C++
89+ // element type.
90+ elementTypeName ?: string
91+ shape ?: GeaIrStoreValueShape
92+ // `this.<field>` reads inside the body — the reactive dependencies that
93+ // should re-run the list when they change.
94+ deps : string [ ]
95+ body : string
96+ ops ?: GeaIrStoreStmt [ ]
97+ sourceSpan ?: GeaIrSourceSpan
98+ }
99+
75100export interface GeaIrStoreField {
76101 name : string
77102 initializer ?: string
@@ -181,7 +206,13 @@ export function storeFieldsToIr(classDecl: ClassDeclaration): GeaIrStoreField[]
181206function arrayElementTypeNameFromAnnotation ( member : import ( '@babel/types' ) . ClassProperty ) : string | undefined {
182207 const annotation = member . typeAnnotation
183208 if ( ! annotation || ! t . isTSTypeAnnotation ( annotation ) ) return undefined
184- const typeNode = annotation . typeAnnotation
209+ return arrayElementTypeNameFromTSType ( annotation . typeAnnotation )
210+ }
211+
212+ // Given a TS type node, return the bare element interface name for `T[]` /
213+ // `Array<T>` / `ReadonlyArray<T>` with a single named-type element; undefined
214+ // for anonymous element types, unions, or non-array types.
215+ function arrayElementTypeNameFromTSType ( typeNode : import ( '@babel/types' ) . TSType ) : string | undefined {
185216 if ( t . isTSArrayType ( typeNode ) ) {
186217 const element = typeNode . elementType
187218 if ( t . isTSTypeReference ( element ) && t . isIdentifier ( element . typeName ) ) return element . typeName . name
@@ -199,6 +230,232 @@ function arrayElementTypeNameFromAnnotation(member: import('@babel/types').Class
199230 return undefined
200231}
201232
233+ const ARRAY_PRODUCING_METHODS = new Set ( [
234+ 'filter' ,
235+ 'map' ,
236+ 'slice' ,
237+ 'concat' ,
238+ 'flat' ,
239+ 'flatMap' ,
240+ 'sort' ,
241+ 'toSorted' ,
242+ 'reverse' ,
243+ 'toReversed' ,
244+ ] )
245+
246+ export function storeGettersToIr ( classDecl : ClassDeclaration ) : GeaIrStoreGetter [ ] {
247+ const getters : GeaIrStoreGetter [ ] = [ ]
248+ // Sibling field shapes, so a getter that derives from an array field
249+ // (`this.bricks.filter(...)`) can borrow that field's element shape.
250+ const fieldShapeByName = new Map < string , GeaIrStoreValueShape > ( )
251+ for ( const field of storeFieldsToIr ( classDecl ) ) {
252+ if ( field . shape ) fieldShapeByName . set ( field . name , field . shape )
253+ }
254+ for ( const member of classDecl . body . body ) {
255+ if ( ! t . isClassMethod ( member ) || member . static || member . computed || member . kind !== 'get' ) continue
256+ if ( ! t . isIdentifier ( member . key ) ) continue
257+ const returnType = member . returnType
258+ const elementTypeName =
259+ returnType && t . isTSTypeAnnotation ( returnType )
260+ ? arrayElementTypeNameFromTSType ( returnType . typeAnnotation )
261+ : undefined
262+ const deps = collectThisFieldReads ( member . body )
263+ const elementShape = getterElementShape ( member . body , deps , elementTypeName , fieldShapeByName )
264+ const returnsArray = ! ! elementTypeName || ! ! elementShape || getterBodyReturnsArray ( member . body )
265+ const shape : GeaIrStoreValueShape | undefined = returnsArray
266+ ? {
267+ kind : 'array' ,
268+ ...( elementShape ? { element : elementShape } : { } ) ,
269+ ...( elementTypeName ? { elementTypeName } : { } ) ,
270+ }
271+ : undefined
272+ const ops = storeStmtsToIr ( member . body . body )
273+ const getter : GeaIrStoreGetter = {
274+ name : member . key . name ,
275+ returnsArray,
276+ deps,
277+ body : generate ( member . body ) . code ,
278+ ...( elementTypeName ? { elementTypeName } : { } ) ,
279+ ...( shape ? { shape } : { } ) ,
280+ ...( ops ? { ops } : { } ) ,
281+ ...( sourceSpan ( member ) ? { sourceSpan : sourceSpan ( member ) } : { } ) ,
282+ }
283+ getters . push ( getter )
284+ }
285+ return getters
286+ }
287+
288+ // Best-effort element (row) object shape for an array-returning getter. Tries
289+ // the body first (an array literal, or a `.map(x => ({...}))` object literal),
290+ // then borrows from a referenced array-of-objects field (`this.bricks.filter`)
291+ // or any field whose element type name matches the getter's return annotation.
292+ function getterElementShape (
293+ body : unknown ,
294+ deps : string [ ] ,
295+ elementTypeName : string | undefined ,
296+ fieldShapeByName : Map < string , GeaIrStoreValueShape > ,
297+ ) : GeaIrStoreValueShape | undefined {
298+ const arg = topLevelReturnArgument ( body )
299+ const fromBody = arg ? elementShapeFromArrayExpression ( arg , fieldShapeByName ) : undefined
300+ if ( fromBody ) return fromBody
301+ if ( elementTypeName ) {
302+ for ( const shape of fieldShapeByName . values ( ) ) {
303+ if ( shape . kind === 'array' && shape . elementTypeName === elementTypeName && shape . element ?. kind === 'object' ) {
304+ return shape . element
305+ }
306+ }
307+ }
308+ for ( const dep of deps ) {
309+ const shape = fieldShapeByName . get ( dep )
310+ if ( shape ?. kind === 'array' && shape . element ?. kind === 'object' ) return shape . element
311+ }
312+ return undefined
313+ }
314+
315+ function elementShapeFromArrayExpression (
316+ expr : unknown ,
317+ fieldShapeByName : Map < string , GeaIrStoreValueShape > ,
318+ ) : GeaIrStoreValueShape | undefined {
319+ if ( t . isArrayExpression ( expr ) ) {
320+ const first = expr . elements . find ( ( element ) => ! ! element && ! t . isSpreadElement ( element ) )
321+ if ( ! first ) return undefined
322+ const shaped = shapeForExpression ( first )
323+ return 'shape' in shaped ? shaped . shape : undefined
324+ }
325+ if ( t . isCallExpression ( expr ) && t . isMemberExpression ( expr . callee ) && t . isIdentifier ( expr . callee . property ) ) {
326+ const method = expr . callee . property . name
327+ if ( method === 'map' ) {
328+ const objectLiteral = mapCallbackObjectLiteral ( expr . arguments [ 0 ] )
329+ if ( ! objectLiteral ) return undefined
330+ const shaped = shapeForExpression ( objectLiteral )
331+ return 'shape' in shaped ? shaped . shape : undefined
332+ }
333+ // filter/slice/sort/etc. preserve element type → recurse into the receiver.
334+ if ( ARRAY_PRODUCING_METHODS . has ( method ) ) {
335+ return elementShapeFromArrayExpression ( expr . callee . object , fieldShapeByName )
336+ }
337+ }
338+ if ( t . isMemberExpression ( expr ) && t . isThisExpression ( expr . object ) && t . isIdentifier ( expr . property ) ) {
339+ const shape = fieldShapeByName . get ( expr . property . name )
340+ if ( shape ?. kind === 'array' ) return shape . element
341+ }
342+ return undefined
343+ }
344+
345+ function mapCallbackObjectLiteral ( callback : unknown ) : import ( '@babel/types' ) . ObjectExpression | undefined {
346+ if ( ! callback || ( ! t . isArrowFunctionExpression ( callback ) && ! t . isFunctionExpression ( callback ) ) ) return undefined
347+ const body = callback . body
348+ if ( t . isObjectExpression ( body ) ) return body
349+ if ( t . isBlockStatement ( body ) ) {
350+ for ( const statement of body . body ) {
351+ if ( t . isReturnStatement ( statement ) && statement . argument && t . isObjectExpression ( statement . argument ) ) {
352+ return statement . argument
353+ }
354+ }
355+ }
356+ return undefined
357+ }
358+
359+ // First `return` argument that is NOT inside a nested function (so a getter's
360+ // own return is found, not a `.map`/`.filter` callback's).
361+ function topLevelReturnArgument ( body : unknown ) : unknown {
362+ let result : unknown
363+ let done = false
364+ const visit = ( value : unknown ) : void => {
365+ if ( done || ! value || typeof value !== 'object' ) return
366+ if ( Array . isArray ( value ) ) {
367+ for ( const item of value ) visit ( item )
368+ return
369+ }
370+ const node = value as Record < string , unknown >
371+ const type = node . type
372+ if ( type === 'FunctionExpression' || type === 'ArrowFunctionExpression' || type === 'FunctionDeclaration' ) return
373+ if ( type === 'ReturnStatement' ) {
374+ result = node . argument
375+ done = true
376+ return
377+ }
378+ for ( const key of Object . keys ( node ) ) {
379+ if ( key === 'loc' || key === 'start' || key === 'end' || key === 'range' ) continue
380+ visit ( node [ key ] )
381+ }
382+ }
383+ visit ( body )
384+ return result
385+ }
386+
387+ // True when a getter body's own `return` yields an array. Nested functions
388+ // (e.g. a `.filter` callback) are skipped so their returns don't count.
389+ function getterBodyReturnsArray ( body : unknown ) : boolean {
390+ let found = false
391+ const visit = ( value : unknown ) : void => {
392+ if ( found || ! value || typeof value !== 'object' ) return
393+ if ( Array . isArray ( value ) ) {
394+ for ( const item of value ) visit ( item )
395+ return
396+ }
397+ const node = value as Record < string , unknown >
398+ const type = node . type
399+ if ( type === 'FunctionExpression' || type === 'ArrowFunctionExpression' || type === 'FunctionDeclaration' ) return
400+ if ( type === 'ReturnStatement' && isArrayProducingExpression ( node . argument ) ) {
401+ found = true
402+ return
403+ }
404+ for ( const key of Object . keys ( node ) ) {
405+ if ( key === 'loc' || key === 'start' || key === 'end' || key === 'range' ) continue
406+ visit ( node [ key ] )
407+ }
408+ }
409+ visit ( body )
410+ return found
411+ }
412+
413+ function isArrayProducingExpression ( expr : unknown ) : boolean {
414+ if ( ! expr || typeof expr !== 'object' ) return false
415+ const node = expr as Record < string , unknown >
416+ if ( node . type === 'ArrayExpression' ) return true
417+ if ( node . type === 'TSAsExpression' || node . type === 'TSNonNullExpression' ) return isArrayProducingExpression ( node . expression )
418+ if ( node . type === 'CallExpression' ) {
419+ const callee = node . callee as Record < string , unknown > | undefined
420+ const property = callee ?. property as Record < string , unknown > | undefined
421+ if ( callee ?. type === 'MemberExpression' && property ?. type === 'Identifier' ) {
422+ return ARRAY_PRODUCING_METHODS . has ( property . name as string )
423+ }
424+ }
425+ return false
426+ }
427+
428+ // Collect the names of `this.<field>` reads anywhere in a node (deduped). These
429+ // are the reactive dependencies of a getter — when any changes, a list derived
430+ // from the getter must recompute.
431+ function collectThisFieldReads ( node : unknown ) : string [ ] {
432+ const names = new Set < string > ( )
433+ const visit = ( value : unknown ) : void => {
434+ if ( ! value || typeof value !== 'object' ) return
435+ if ( Array . isArray ( value ) ) {
436+ for ( const item of value ) visit ( item )
437+ return
438+ }
439+ const rec = value as Record < string , unknown >
440+ const object = rec . object as Record < string , unknown > | undefined
441+ const property = rec . property as Record < string , unknown > | undefined
442+ if (
443+ rec . type === 'MemberExpression' &&
444+ object ?. type === 'ThisExpression' &&
445+ rec . computed !== true &&
446+ property ?. type === 'Identifier'
447+ ) {
448+ names . add ( property . name as string )
449+ }
450+ for ( const key of Object . keys ( rec ) ) {
451+ if ( key === 'loc' || key === 'start' || key === 'end' || key === 'range' ) continue
452+ visit ( rec [ key ] )
453+ }
454+ }
455+ visit ( node )
456+ return [ ...names ]
457+ }
458+
202459export function storeMethodsToIr ( classDecl : ClassDeclaration ) : GeaIrStoreMethod [ ] {
203460 const methods : GeaIrStoreMethod [ ] = [ ]
204461 for ( const member of classDecl . body . body ) {
0 commit comments