From 9f37cd6bbd0713d9f07465635eb3c284eaa2e7fe Mon Sep 17 00:00:00 2001 From: Jiakai Shi Date: Sat, 21 Jun 2025 20:59:16 +0800 Subject: [PATCH] support framework Signed-off-by: Jiakai Shi Change-Id: I1b7194fa7698aaa86e721d8ce353a268ab3f1846 --- arkui-plugins/common/plugin-context.ts | 1 + arkui-plugins/common/predefines.ts | 7 +- .../memo-plugins/function-transformer.ts | 33 +++- arkui-plugins/memo-plugins/index.ts | 25 ++- .../memo-plugins/internal-transformer.ts | 49 +++++ arkui-plugins/memo-plugins/utils.ts | 10 +- .../memo/functions/complex-memo-intrinsic.ets | 74 +++++++ .../mock/memo/functions/internal-memo-arg.ets | 101 ++++++++++ .../complex-memo-intrinsic.test.ts | 151 +++++++++++++++ .../internal-memo-arg.test.ts | 182 ++++++++++++++++++ .../internal-calls.test.ts | 4 +- .../non-void-method.test.ts | 4 +- .../test/utils/plugins/memo-no-recheck.ts | 3 + arkui-plugins/ui-plugins/index.ts | 24 ++- arkui-plugins/ui-syntax-plugins/index.ts | 7 +- koala-wrapper/native/src/bridges.cc | 23 +++ koala-wrapper/src/Es2pandaNativeModule.ts | 4 + koala-wrapper/src/arkts-api/peers/Context.ts | 15 +- .../src/arkts-api/utilities/public.ts | 2 +- 19 files changed, 692 insertions(+), 27 deletions(-) create mode 100644 arkui-plugins/memo-plugins/internal-transformer.ts create mode 100644 arkui-plugins/test/demo/mock/memo/functions/complex-memo-intrinsic.ets create mode 100644 arkui-plugins/test/demo/mock/memo/functions/internal-memo-arg.ets create mode 100644 arkui-plugins/test/ut/memo-plugins/function-declarations/complex-memo-intrinsic.test.ts create mode 100644 arkui-plugins/test/ut/memo-plugins/function-declarations/internal-memo-arg.test.ts diff --git a/arkui-plugins/common/plugin-context.ts b/arkui-plugins/common/plugin-context.ts index 7fddbadc1..9aa704871 100644 --- a/arkui-plugins/common/plugin-context.ts +++ b/arkui-plugins/common/plugin-context.ts @@ -107,6 +107,7 @@ export interface ProjectConfig { projectPath: string, projectRootPath: string, integratedHsp: boolean + frameworkMode?: string; } export type PluginHandlerFunction = () => void; diff --git a/arkui-plugins/common/predefines.ts b/arkui-plugins/common/predefines.ts index 7cc1eb969..cf481fd9b 100644 --- a/arkui-plugins/common/predefines.ts +++ b/arkui-plugins/common/predefines.ts @@ -24,10 +24,15 @@ export const EXTERNAL_SOURCE_PREFIX_NAMES: (string | RegExp)[] = [ /@arkts\..*/, /@ohos\.(?!arkui).*/, /@system\..*/, - /arkui\.(?!Ark|[Uu]serView$)[A-Z]/, // temporary solution /ability\..*/, ]; +export const EXTERNAL_SOURCE_PREFIX_NAMES_FOR_FRAMEWORK: (string | RegExp)[] = [ + 'std', + 'escompat', + /@arkts\..*/ +]; + export const ARKUI_IMPORT_PREFIX_NAMES: (string | RegExp)[] = [/arkui\..*/, /@ohos\..*/, /@kit\..*/]; export const MEMO_IMPORT_SOURCE_NAME: string = 'arkui.stateManagement.runtime'; diff --git a/arkui-plugins/memo-plugins/function-transformer.ts b/arkui-plugins/memo-plugins/function-transformer.ts index fb059b409..c48edac98 100644 --- a/arkui-plugins/memo-plugins/function-transformer.ts +++ b/arkui-plugins/memo-plugins/function-transformer.ts @@ -48,12 +48,13 @@ import { isStandaloneArrowFunction, isThisAttributeAssignment, removeMemoAnnotation, - parametrizedNodeHasReceiver + parametrizedNodeHasReceiver, } from './utils'; import { ParameterTransformer } from './parameter-transformer'; import { ReturnTransformer } from './return-transformer'; import { SignatureTransformer } from './signature-transformer'; import { moveToFront } from '../common/arkts-utils'; +import { InternalsTransformer } from './internal-transformer'; interface ScopeInfo extends MemoInfo { regardAsSameScope?: boolean; @@ -64,6 +65,7 @@ export interface FunctionTransformerOptions extends VisitorOptions { parameterTransformer: ParameterTransformer; returnTransformer: ReturnTransformer; signatureTransformer: SignatureTransformer; + internalsTransformer?: InternalsTransformer; } export class FunctionTransformer extends AbstractVisitor { @@ -71,6 +73,7 @@ export class FunctionTransformer extends AbstractVisitor { private readonly parameterTransformer: ParameterTransformer; private readonly returnTransformer: ReturnTransformer; private readonly signatureTransformer: SignatureTransformer; + private readonly internalsTransformer?: InternalsTransformer; /* Tracking whether should import `__memo_context_type` and `__memo_id_type` */ private modified = false; @@ -81,6 +84,7 @@ export class FunctionTransformer extends AbstractVisitor { this.parameterTransformer = options.parameterTransformer; this.returnTransformer = options.returnTransformer; this.signatureTransformer = options.signatureTransformer; + this.internalsTransformer = options.internalsTransformer; } private scopes: ScopeInfo[] = []; @@ -224,15 +228,36 @@ export class FunctionTransformer extends AbstractVisitor { return this; } + updateInternalsInScriptFunction(scriptFunction: arkts.ScriptFunction): arkts.ScriptFunction { + if (!scriptFunction.body || !arkts.isBlockStatement(scriptFunction.body) || !this.internalsTransformer) { + return scriptFunction; + } + const afterInternalsTransformer = this.internalsTransformer.visitor( + scriptFunction.body + ) as arkts.BlockStatement; + return arkts.factory.updateScriptFunction( + scriptFunction, + afterInternalsTransformer, + arkts.factory.createFunctionSignature( + scriptFunction.typeParams, + scriptFunction.params, + scriptFunction.returnTypeAnnotation, + scriptFunction.hasReceiver + ), + scriptFunction.flags, + scriptFunction.modifiers + ); + } + updateScriptFunction(scriptFunction: arkts.ScriptFunction, name: string = ''): arkts.ScriptFunction { if (!scriptFunction.body || !arkts.isBlockStatement(scriptFunction.body)) { return scriptFunction; } if (this.parameterTransformer.isTracked(scriptFunction.body)) { - return scriptFunction; + return this.updateInternalsInScriptFunction(scriptFunction); } if (hasMemoIntrinsicAnnotation(scriptFunction) || hasMemoEntryAnnotation(scriptFunction)) { - return scriptFunction; + return this.updateInternalsInScriptFunction(scriptFunction); } const returnType = scriptFunction.returnTypeAnnotation; const isStableThis = this.stable > 0 && returnType !== undefined && arkts.isTSThisType(returnType); @@ -264,7 +289,7 @@ export class FunctionTransformer extends AbstractVisitor { ); this.modified = true; this.parameterTransformer.track(updateScriptFunction.body); - return updateScriptFunction; + return this.updateInternalsInScriptFunction(updateScriptFunction); } private updateMethodDefinition(node: arkts.MethodDefinition): arkts.MethodDefinition { diff --git a/arkui-plugins/memo-plugins/index.ts b/arkui-plugins/memo-plugins/index.ts index 243d8b85f..9d5c2ed4d 100644 --- a/arkui-plugins/memo-plugins/index.ts +++ b/arkui-plugins/memo-plugins/index.ts @@ -20,9 +20,10 @@ import { PositionalIdTracker } from './utils'; import { ReturnTransformer } from './return-transformer'; import { ParameterTransformer } from './parameter-transformer'; import { ProgramVisitor } from '../common/program-visitor'; -import { EXTERNAL_SOURCE_PREFIX_NAMES } from '../common/predefines'; +import { EXTERNAL_SOURCE_PREFIX_NAMES, EXTERNAL_SOURCE_PREFIX_NAMES_FOR_FRAMEWORK } from '../common/predefines'; import { debugDump, debugLog, getDumpFileName } from '../common/debug'; import { SignatureTransformer } from './signature-transformer'; +import { InternalsTransformer } from './internal-transformer'; export function unmemoizeTransform(): Plugins { return { @@ -44,6 +45,8 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { let script = program.astNode; debugLog('[BEFORE MEMO SCRIPT] script: ', script.dumpSrc()); const cachePath: string | undefined = this.getProjectConfig()?.cachePath; + const isFrameworkMode = !!this.getProjectConfig()?.frameworkMode; + const canSkipPhases = !isFrameworkMode && program.canSkipPhases(); debugDump( script.dumpSrc(), getDumpFileName(0, 'SRC', 5, 'MEMO_AfterCheck_Begin'), @@ -52,7 +55,7 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { program.fileNameWithExtension ); arkts.Performance.getInstance().createEvent('memo-checked'); - program = checkedProgramVisit(program, this); + program = checkedProgramVisit(program, this, canSkipPhases, isFrameworkMode); script = program.astNode; arkts.Performance.getInstance().stopEvent('memo-checked', true); debugLog('[AFTER MEMO SCRIPT] script: ', script.dumpSrc()); @@ -82,8 +85,13 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { return undefined; } -function checkedProgramVisit(program: arkts.Program, context: PluginContext): arkts.Program { - if (program.canSkipPhases()) { +function checkedProgramVisit( + program: arkts.Program, + pluginContext: PluginContext, + canSkipPhases: boolean = false, + isFrameworkMode: boolean = false +): arkts.Program { + if (canSkipPhases) { debugLog('[SKIP PHASE] phase: memo-checked, moduleName: ', program.moduleName); } else { debugLog('[CANT SKIP PHASE] phase: memo-checked, moduleName: ', program.moduleName); @@ -91,18 +99,23 @@ function checkedProgramVisit(program: arkts.Program, context: PluginContext): ar const parameterTransformer = new ParameterTransformer({ positionalIdTracker }); const returnTransformer = new ReturnTransformer(); const signatureTransformer = new SignatureTransformer(); + const internalsTransformer = new InternalsTransformer({ positionalIdTracker }); const functionTransformer = new FunctionTransformer({ positionalIdTracker, parameterTransformer, returnTransformer, signatureTransformer, + internalsTransformer, }); + const skipPrefixNames = isFrameworkMode + ? EXTERNAL_SOURCE_PREFIX_NAMES_FOR_FRAMEWORK + : EXTERNAL_SOURCE_PREFIX_NAMES; const programVisitor = new ProgramVisitor({ pluginName: unmemoizeTransform.name, state: arkts.Es2pandaContextState.ES2PANDA_STATE_CHECKED, visitors: [functionTransformer], - skipPrefixNames: EXTERNAL_SOURCE_PREFIX_NAMES, - pluginContext: context, + skipPrefixNames, + pluginContext, }); program = programVisitor.programVisitor(program); } diff --git a/arkui-plugins/memo-plugins/internal-transformer.ts b/arkui-plugins/memo-plugins/internal-transformer.ts new file mode 100644 index 000000000..3541fb7c3 --- /dev/null +++ b/arkui-plugins/memo-plugins/internal-transformer.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) 2022-2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as arkts from '@koalaui/libarkts'; +import { RuntimeNames, PositionalIdTracker } from './utils'; +import { AbstractVisitor, VisitorOptions } from '../common/abstract-visitor'; + +export interface InternalsTransformerOptions extends VisitorOptions { + positionalIdTracker: PositionalIdTracker; +} + +export class InternalsTransformer extends AbstractVisitor { + private readonly positionalIdTracker: PositionalIdTracker; + + constructor(options: InternalsTransformerOptions) { + super(options); + this.positionalIdTracker = options.positionalIdTracker; + } + + visitor(beforeChildren: arkts.AstNode): arkts.AstNode { + const node = this.visitEachChild(beforeChildren); + if (arkts.isCallExpression(node)) { + if (arkts.isIdentifier(node.expression)) { + if (node.expression.name === RuntimeNames.__CONTEXT) { + return arkts.factory.createIdentifier(RuntimeNames.CONTEXT, undefined); + } + if (node.expression.name === RuntimeNames.__ID) { + return arkts.factory.createIdentifier(RuntimeNames.ID, undefined); + } + if (node.expression.name === RuntimeNames.__KEY) { + return this.positionalIdTracker.id(RuntimeNames.__KEY); + } + } + } + return node; + } +} diff --git a/arkui-plugins/memo-plugins/utils.ts b/arkui-plugins/memo-plugins/utils.ts index bbd906a2d..526b594d1 100644 --- a/arkui-plugins/memo-plugins/utils.ts +++ b/arkui-plugins/memo-plugins/utils.ts @@ -20,6 +20,7 @@ const UniqueId = common.UniqueId; export enum RuntimeNames { __CONTEXT = '__context', __ID = '__id', + __KEY = "__key", ANNOTATION_BUILDER = 'Builder', ANNOTATION = 'memo', ANNOTATION_ENTRY = 'memo_entry', @@ -451,9 +452,14 @@ export function findMemoFromTypeAnnotation(typeAnnotation: arkts.AstNode | undef } if (arkts.isETSTypeReference(typeAnnotation) && !!typeAnnotation.part && !!typeAnnotation.part.name) { let decl: arkts.AstNode | undefined = arkts.getDecl(typeAnnotation.part.name); - if (!!decl && arkts.isTSTypeAliasDeclaration(decl)) { - return hasMemoAnnotation(decl) || hasMemoIntrinsicAnnotation(decl) || decl.typeAnnotation ? findMemoFromTypeAnnotation(decl.typeAnnotation) : false; + if (!decl || !arkts.isTSTypeAliasDeclaration(decl)) { + return false; + } + let isMemo: boolean = hasMemoAnnotation(decl) || hasMemoIntrinsicAnnotation(decl); + if (!isMemo && !!decl.typeAnnotation) { + isMemo = findMemoFromTypeAnnotation(decl.typeAnnotation); } + return isMemo; } else if (arkts.isETSFunctionType(typeAnnotation)) { return hasMemoAnnotation(typeAnnotation) || hasMemoIntrinsicAnnotation(typeAnnotation); } else if (arkts.isETSUnionType(typeAnnotation)) { diff --git a/arkui-plugins/test/demo/mock/memo/functions/complex-memo-intrinsic.ets b/arkui-plugins/test/demo/mock/memo/functions/complex-memo-intrinsic.ets new file mode 100644 index 000000000..1ab1a5eba --- /dev/null +++ b/arkui-plugins/test/demo/mock/memo/functions/complex-memo-intrinsic.ets @@ -0,0 +1,74 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { memo } from "arkui.stateManagement.runtime"; + +@Retention({policy:"SOURCE"}) @interface memo_intrinsic {} + +interface IA { + ccc: boolean; +} + +class A implements IA { + bbb: Map = new Map(); + ddd: Map = new Map(); + ccc: boolean = false; + + aaa(value: number): void {} +} + +export type SimpleArray = Array | ReadonlyArray | Readonly>; + +@memo_intrinsic +export declare function factory(compute: () => Value): Value; + +export function cb(callback?: () => void) { + if (callback) return; +} + +@memo_intrinsic +export function impl( + @memo + style: ((attributes: IA) => void) | undefined, + arr: SimpleArray, + err: string = 'error message' +): void { + const s = factory(() => { + return new A(); + }); + s.aaa(arr.length); + style?.(s); + if (!s.bbb.get('some_key')) { + throw new Error(err); + } + if (s.ccc) { + cb(() => + s.ddd.forEach((s: number, t: string) => { + console.log(err); + return; + }) + ); + } else { + return; + } +} + +class Use { + @memo test() { + const style = @memo (attributes: IA) => {}; + const arr = [1, 2, 3, 4]; + impl(style, arr); + } +} \ No newline at end of file diff --git a/arkui-plugins/test/demo/mock/memo/functions/internal-memo-arg.ets b/arkui-plugins/test/demo/mock/memo/functions/internal-memo-arg.ets new file mode 100644 index 000000000..38f146018 --- /dev/null +++ b/arkui-plugins/test/demo/mock/memo/functions/internal-memo-arg.ets @@ -0,0 +1,101 @@ +/* + * Copyright (C) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { memo, __memo_context_type, __memo_id_type } from "arkui.stateManagement.runtime"; +import { IncrementalNode } from "@koalaui.runtime.tree.IncrementalNode"; +import { ControlledScope, StateManager } from "@koalaui.runtime.states.State"; + +@Retention({policy:"SOURCE"}) @interface memo_intrinsic {} +@Retention({policy:"SOURCE"}) @interface memo_entry {} + +export declare function __context(): __memo_context_type +export declare function __id(): __memo_id_type + +@memo_entry() export function memoEntry(__memo_context: __memo_context_type, __memo_id: __memo_id_type, @memo() entry: (()=> R)): R { + return entry(); +} + +@memo_entry() export function memoEntry1(__memo_context: __memo_context_type, __memo_id: __memo_id_type, @memo() entry: ((arg: T)=> R), arg: T): R { + return entry(arg); +} + +@memo_entry() export function memoEntry2(__memo_context: __memo_context_type, __memo_id: __memo_id_type, @memo() entry: ((arg1: T1, arg2: T2)=> R), arg1: T1, arg2: T2): R { + return entry(arg1, arg2); +} + +export class MemoCallbackContext { + private readonly context: __memo_context_type; + + private readonly id: __memo_id_type; + + private constructor(context: __memo_context_type, id: __memo_id_type) { + this.context = context; + this.id = id; + } + + @memo() public static Make(): MemoCallbackContext { + return new MemoCallbackContext(__context(), __id()); + } +} + +@memo_intrinsic() export function contextLocalValue(name: string): Value { + return __context().valueBy(name); +} + +@memo_intrinsic() export function contextLocalScope(name: string, value: Value, @memo() content: (()=> void)) { + const scope = __context().scope(__id(), 1); + scope.param(0, value, undefined, name, true); + if (scope.unchanged) { + scope.cached; + } else { + content(); + scope.recache(); + } +} + +@memo_intrinsic() export function NodeAttach(create: (()=> Node), @memo() update: ((node: Node)=> void), reuseKey?: string): void { + const scope = __context().scope(__id(), 0, create, undefined, undefined, undefined, reuseKey); + if (scope.unchanged) { + scope.cached; + } else { + try { + if (!reuseKey) { + update((__context().node as Node)) + } else { + memoEntry(__context(), 0, (() => { + update((__context().node as Node)); + })) + } + } finally { + scope.recache(); + } + } +} + +@memo_intrinsic() export function rememberControlledScope(invalidate: (()=> void)): ControlledScope { + return __context().controlledScope(__id(), invalidate); +} + +@memo() export function Repeat(count: int, @memo() action: ((index: int)=> void)) { + for (let i = 0;((i) < (count));(i++)) { + memoEntry1(__context(), i, action, i); + } +} + +export class CustomComponent { + @memo() public static _instantiateImpl(): void { + const context: StateManager = (__context() as StateManager); + } +} diff --git a/arkui-plugins/test/ut/memo-plugins/function-declarations/complex-memo-intrinsic.test.ts b/arkui-plugins/test/ut/memo-plugins/function-declarations/complex-memo-intrinsic.test.ts new file mode 100644 index 000000000..f5175c19a --- /dev/null +++ b/arkui-plugins/test/ut/memo-plugins/function-declarations/complex-memo-intrinsic.test.ts @@ -0,0 +1,151 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import { PluginTester } from '../../../utils/plugin-tester'; +import { mockBuildConfig } from '../../../utils/artkts-config'; +import { getRootPath, MOCK_ENTRY_DIR_PATH } from '../../../utils/path-config'; +import { parseDumpSrc } from '../../../utils/parse-string'; +import { memoNoRecheck, recheck } from '../../../utils/plugins'; +import { BuildConfig, PluginTestContext } from '../../../utils/shared-types'; + +const FUNCTION_DIR_PATH: string = 'memo/functions'; + +const buildConfig: BuildConfig = mockBuildConfig(); +buildConfig.compileFiles = [ + path.resolve(getRootPath(), MOCK_ENTRY_DIR_PATH, FUNCTION_DIR_PATH, 'complex-memo-intrinsic.ets'), +]; + +const pluginTester = new PluginTester('test complex memo intrinsic function', buildConfig); + +const expectedScript: string = ` + +import { __memo_context_type as __memo_context_type, __memo_id_type as __memo_id_type } from "arkui.stateManagement.runtime"; + +import { memo as memo } from "arkui.stateManagement.runtime"; + +function main() {} + + +@memo_intrinsic() export function factory(__memo_context: __memo_context_type, __memo_id: __memo_id_type, compute: (()=> Value)): Value + +export function cb(callback?: (()=> void)) { + if (callback) { + return; + } +} + +@memo_intrinsic() export function impl(__memo_context: __memo_context_type, __memo_id: __memo_id_type, @memo() style: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, attributes: IA)=> void) | undefined, arr: SimpleArray, gensym%%_1?: string): void { + let err: string = (((gensym%%_1) !== (undefined)) ? gensym%%_1 : ("error message" as string)); + const s = factory(__memo_context, ((__memo_id) + (90010973)), (() => { + return new A(); + })); + s.aaa(arr.length); + ({let gensym%%_68 = style; + (((gensym%%_68) == (null)) ? undefined : gensym%%_68(__memo_context, ((__memo_id) + (222201816)), s))}); + if (!(s.bbb.get("some_key"))) { + throw new Error(err); + } + if (s.ccc) { + cb((() => { + return s.ddd.forEach(((s: number, t: string) => { + console.log(err); + return; + })); + })); + } else { + return; + } +} + +@Retention({policy:"SOURCE"}) @interface memo_intrinsic {} + +interface IA { + set ccc(ccc: boolean) + + get ccc(): boolean + +} + +class A implements IA { + public bbb: Map = new Map(); + + public ddd: Map = new Map(); + + public aaa(value: number): void {} + + public constructor() {} + + private _$property$_ccc: boolean = false; + + set ccc(_$property$_ccc: boolean) { + this._$property$_ccc = _$property$_ccc; + return; + } + + public get ccc(): boolean { + return this._$property$_ccc; + } + +} + +export type SimpleArray = Array | ReadonlyArray | Readonly>; + +class Use { + public test(__memo_context: __memo_context_type, __memo_id: __memo_id_type): void { + const __memo_scope = __memo_context.scope(((__memo_id) + (228150357)), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + const style = @memo() ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, attributes: IA): void => { + const __memo_scope = __memo_context.scope(((__memo_id) + (237001330)), 1); + const __memo_parameter_attributes = __memo_scope.param(0, attributes); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + { + __memo_scope.recache(); + return; + } + }); + const arr = [1, 2, 3, 4]; + impl(__memo_context, ((__memo_id) + (158199735)), style, arr); + { + __memo_scope.recache(); + return; + } + } + + public constructor() {} + +} +`; + +function testMemoTransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedScript)); +} + +pluginTester.run( + 'transform complex @memo_intrinsic calls in functions', + [memoNoRecheck, recheck], + { + 'checked:memo-no-recheck': [testMemoTransformer], + }, + { + stopAfter: 'checked', + } +); diff --git a/arkui-plugins/test/ut/memo-plugins/function-declarations/internal-memo-arg.test.ts b/arkui-plugins/test/ut/memo-plugins/function-declarations/internal-memo-arg.test.ts new file mode 100644 index 000000000..dd7df949f --- /dev/null +++ b/arkui-plugins/test/ut/memo-plugins/function-declarations/internal-memo-arg.test.ts @@ -0,0 +1,182 @@ +/* + * Copyright (c) 2025 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import * as path from 'path'; +import { PluginTester } from '../../../utils/plugin-tester'; +import { mockBuildConfig } from '../../../utils/artkts-config'; +import { getRootPath, MOCK_ENTRY_DIR_PATH } from '../../../utils/path-config'; +import { parseDumpSrc } from '../../../utils/parse-string'; +import { memoNoRecheck, recheck } from '../../../utils/plugins'; +import { BuildConfig, PluginTestContext } from '../../../utils/shared-types'; + +const FUNCTION_DIR_PATH: string = 'memo/functions'; + +const buildConfig: BuildConfig = mockBuildConfig(); +buildConfig.compileFiles = [ + path.resolve(getRootPath(), MOCK_ENTRY_DIR_PATH, FUNCTION_DIR_PATH, 'internal-memo-arg.ets'), +]; + +const pluginTester = new PluginTester('test internal memo argument calls', buildConfig); + +const expectedScript: string = ` + +import { __memo_context_type as __memo_context_type, __memo_id_type as __memo_id_type } from "arkui.stateManagement.runtime"; + +import { memo as memo, __memo_context_type as __memo_context_type, __memo_id_type as __memo_id_type } from "arkui.stateManagement.runtime"; + +import { IncrementalNode as IncrementalNode } from "@koalaui.runtime.tree.IncrementalNode"; + +import { ControlledScope as ControlledScope, StateManager as StateManager } from "@koalaui.runtime.states.State"; + +function main() {} + + +export function __context(): __memo_context_type + +export function __id(): __memo_id_type + +@memo_entry() export function memoEntry(__memo_context: __memo_context_type, __memo_id: __memo_id_type, @memo() entry: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type)=> R)): R { + return entry(__memo_context, ((__memo_id) + (75311131))); +} + +@memo_entry() export function memoEntry1(__memo_context: __memo_context_type, __memo_id: __memo_id_type, @memo() entry: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, arg: T)=> R), arg: T): R { + return entry(__memo_context, ((__memo_id) + (168506859)), arg); +} + +@memo_entry() export function memoEntry2(__memo_context: __memo_context_type, __memo_id: __memo_id_type, @memo() entry: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, arg1: T1, arg2: T2)=> R), arg1: T1, arg2: T2): R { + return entry(__memo_context, ((__memo_id) + (76962895)), arg1, arg2); +} + +@memo_intrinsic() export function contextLocalValue(__memo_context: __memo_context_type, __memo_id: __memo_id_type, name: string): Value { + return __memo_context.valueBy(name); +} + +@memo_intrinsic() export function contextLocalScope(__memo_context: __memo_context_type, __memo_id: __memo_id_type, name: string, value: Value, @memo() content: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type)=> void)): void { + const scope = __memo_context.scope(__memo_id, 1); + scope.param(0, value, undefined, name, true); + if (scope.unchanged) { + scope.cached; + } else { + content(__memo_context, ((__memo_id) + (2633070))); + scope.recache(); + } +} + +@memo_intrinsic() export function NodeAttach(__memo_context: __memo_context_type, __memo_id: __memo_id_type, create: (()=> Node), @memo() update: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, node: Node)=> void), reuseKey?: string): void { + const scope = __memo_context.scope(__memo_id, 0, create, undefined, undefined, undefined, reuseKey); + if (scope.unchanged) { + scope.cached; + } else { + try { + if (!reuseKey) { + update(__memo_context, ((__memo_id) + (6025780)), (__memo_context.node as Node)); + } else { + memoEntry(__memo_context, 0, ((__memo_context: __memo_context_type, __memo_id: __memo_id_type): void => { + const __memo_scope = __memo_context.scope(((__memo_id) + (31840240)), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + update(__memo_context, ((__memo_id) + (253864074)), (__memo_context.node as Node)); + { + __memo_scope.recache(); + return; + } + })); + } + } finally { + scope.recache(); + } + } +} + +@memo_intrinsic() export function rememberControlledScope(__memo_context: __memo_context_type, __memo_id: __memo_id_type, invalidate: (()=> void)): ControlledScope { + return __memo_context.controlledScope(__memo_id, invalidate); +} + +export function Repeat(__memo_context: __memo_context_type, __memo_id: __memo_id_type, count: int, @memo() action: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type, index: int)=> void)): void { + const __memo_scope = __memo_context.scope(((__memo_id) + (200707415)), 2); + const __memo_parameter_count = __memo_scope.param(0, count), __memo_parameter_action = __memo_scope.param(1, action); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + for (let i = 0;((i) < (__memo_parameter_count.value));(i++)) { + memoEntry1(__memo_context, i, __memo_parameter_action.value, i); + } + { + __memo_scope.recache(); + return; + } +} + + +@Retention({policy:"SOURCE"}) @interface memo_intrinsic {} + +@Retention({policy:"SOURCE"}) @interface memo_entry {} + +export class MemoCallbackContext { + private readonly context: __memo_context_type; + + private readonly id: __memo_id_type; + + private constructor(context: __memo_context_type, id: __memo_id_type) { + this.context = context; + this.id = id; + } + + public static Make(__memo_context: __memo_context_type, __memo_id: __memo_id_type): MemoCallbackContext { + const __memo_scope = __memo_context.scope(((__memo_id) + (41727473)), 0); + if (__memo_scope.unchanged) { + return __memo_scope.cached; + } + return __memo_scope.recache(new MemoCallbackContext(__memo_context, __memo_id)); + } + +} + +export class CustomComponent { + public static _instantiateImpl(__memo_context: __memo_context_type, __memo_id: __memo_id_type): void { + const __memo_scope = __memo_context.scope(((__memo_id) + (214802466)), 0); + if (__memo_scope.unchanged) { + __memo_scope.cached; + return; + } + const context: StateManager = (__memo_context as StateManager); + { + __memo_scope.recache(); + return; + } + } + + public constructor() {} + +} +`; + +function testMemoTransformer(this: PluginTestContext): void { + expect(parseDumpSrc(this.scriptSnapshot ?? '')).toBe(parseDumpSrc(expectedScript)); +} + +pluginTester.run( + 'transform internal __context() and __id() calls in functions', + [memoNoRecheck, recheck], + { + 'checked:memo-no-recheck': [testMemoTransformer], + }, + { + stopAfter: 'checked', + } +); diff --git a/arkui-plugins/test/ut/memo-plugins/method-declarations/internal-calls.test.ts b/arkui-plugins/test/ut/memo-plugins/method-declarations/internal-calls.test.ts index 022c01ac0..1c870cf48 100644 --- a/arkui-plugins/test/ut/memo-plugins/method-declarations/internal-calls.test.ts +++ b/arkui-plugins/test/ut/memo-plugins/method-declarations/internal-calls.test.ts @@ -65,8 +65,8 @@ class Test { __memo_scope.cached; return; } - __context(); - __id(); + __memo_context; + __memo_id; { __memo_scope.recache(); return; diff --git a/arkui-plugins/test/ut/memo-plugins/method-declarations/non-void-method.test.ts b/arkui-plugins/test/ut/memo-plugins/method-declarations/non-void-method.test.ts index 962129e7d..70a12bc70 100644 --- a/arkui-plugins/test/ut/memo-plugins/method-declarations/non-void-method.test.ts +++ b/arkui-plugins/test/ut/memo-plugins/method-declarations/non-void-method.test.ts @@ -73,10 +73,10 @@ class Test { } @memo_entry() public memoEntry(__memo_context: __memo_context_type, __memo_id: __memo_id_type, @memo() entry: ((__memo_context: __memo_context_type, __memo_id: __memo_id_type)=> R)): R { const getContext = (() => { - return __context(); + return __memo_context; }); const getId = (() => { - return __id(); + return __memo_id; }); { const __memo_context = getContext(); diff --git a/arkui-plugins/test/utils/plugins/memo-no-recheck.ts b/arkui-plugins/test/utils/plugins/memo-no-recheck.ts index c953249a1..f4bc38681 100644 --- a/arkui-plugins/test/utils/plugins/memo-no-recheck.ts +++ b/arkui-plugins/test/utils/plugins/memo-no-recheck.ts @@ -22,6 +22,7 @@ import { ParameterTransformer } from '../../../memo-plugins/parameter-transforme import { ReturnTransformer } from '../../../memo-plugins/return-transformer'; import { SignatureTransformer } from '../../../memo-plugins/signature-transformer'; import { FunctionTransformer } from '../../../memo-plugins/function-transformer'; +import { InternalsTransformer } from '../../../memo-plugins/internal-transformer'; /** * AfterCheck unmemoizeTransform with no recheck AST. @@ -39,11 +40,13 @@ export const memoNoRecheck: Plugins = { }); const returnTransformer = new ReturnTransformer(); const signatureTransformer = new SignatureTransformer(); + const internalsTransformer = new InternalsTransformer({ positionalIdTracker }); const functionTransformer = new FunctionTransformer({ positionalIdTracker, parameterTransformer, returnTransformer, signatureTransformer, + internalsTransformer, }); const programVisitor = new ProgramVisitor({ pluginName: memoNoRecheck.name, diff --git a/arkui-plugins/ui-plugins/index.ts b/arkui-plugins/ui-plugins/index.ts index 5728f6a80..792ca0de8 100644 --- a/arkui-plugins/ui-plugins/index.ts +++ b/arkui-plugins/ui-plugins/index.ts @@ -43,6 +43,8 @@ function parsedTransform(this: PluginContext): arkts.EtsScript | undefined { let program = arkts.getOrUpdateGlobalContext(contextPtr).program; script = program.astNode; const cachePath: string | undefined = this.getProjectConfig()?.cachePath; + const isFrameworkMode = this.getProjectConfig()?.frameworkMode; + const canSkipPhases = !!isFrameworkMode || program.canSkipPhases(); debugLog('[BEFORE PARSED SCRIPT] script: ', script.dumpSrc()); debugDump( script.dumpSrc(), @@ -52,7 +54,7 @@ function parsedTransform(this: PluginContext): arkts.EtsScript | undefined { program.fileNameWithExtension ); arkts.Performance.getInstance().createEvent('ui-parsed'); - program = parsedProgramVisit(program, this); + program = parsedProgramVisit(program, this, canSkipPhases); script = program.astNode; arkts.Performance.getInstance().stopEvent('ui-parsed', true); debugLog('[AFTER PARSED SCRIPT] script: ', script.dumpSrc()); @@ -74,8 +76,12 @@ function parsedTransform(this: PluginContext): arkts.EtsScript | undefined { return script; } -function parsedProgramVisit(program: arkts.Program, context: PluginContext): arkts.Program { - if (program.canSkipPhases()) { +function parsedProgramVisit( + program: arkts.Program, + context: PluginContext, + canSkipPhases: boolean = false +): arkts.Program { + if (canSkipPhases) { debugLog('[SKIP PHASE] phase: ui-parsed, moduleName: ', program.moduleName); } else { debugLog('[CANT SKIP PHASE] phase: ui-parsed, moduleName: ', program.moduleName); @@ -106,6 +112,8 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { let program = arkts.getOrUpdateGlobalContext(contextPtr).program; script = program.astNode; const cachePath: string | undefined = this.getProjectConfig()?.cachePath; + const isFrameworkMode = this.getProjectConfig()?.frameworkMode; + const canSkipPhases = !!isFrameworkMode || program.canSkipPhases(); debugLog('[BEFORE STRUCT SCRIPT] script: ', script.dumpSrc()); debugDump( script.dumpSrc(), @@ -115,7 +123,7 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { program.fileNameWithExtension ); arkts.Performance.getInstance().createEvent('ui-checked'); - program = checkedProgramVisit(program, this); + program = checkedProgramVisit(program, this, canSkipPhases); script = program.astNode; arkts.Performance.getInstance().stopEvent('ui-checked', true); debugLog('[AFTER STRUCT SCRIPT] script: ', script.dumpSrc()); @@ -139,8 +147,12 @@ function checkedTransform(this: PluginContext): arkts.EtsScript | undefined { return script; } -function checkedProgramVisit(program: arkts.Program, context: PluginContext): arkts.Program { - if (program.canSkipPhases()) { +function checkedProgramVisit( + program: arkts.Program, + context: PluginContext, + canSkipPhases: boolean = false +): arkts.Program { + if (canSkipPhases) { debugLog('[SKIP PHASE] phase: ui-checked, moduleName: ', program.moduleName); } else { debugLog('[CANT SKIP PHASE] phase: ui-checked, moduleName: ', program.moduleName); diff --git a/arkui-plugins/ui-syntax-plugins/index.ts b/arkui-plugins/ui-syntax-plugins/index.ts index 36167ae8c..5447998e2 100644 --- a/arkui-plugins/ui-syntax-plugins/index.ts +++ b/arkui-plugins/ui-syntax-plugins/index.ts @@ -29,9 +29,14 @@ export function uiSyntaxLinterTransform(): Plugins { return undefined; } const program = arkts.getOrUpdateGlobalContext(contextPtr).program; + const projectConfig = this.getProjectConfig(); + const isFrameworkMode = this.getProjectConfig()?.frameworkMode; + const canSkipPhases = !isFrameworkMode; + if (canSkipPhases) { + return undefined; + } const node = program.astNode; if (node) { - const projectConfig = this.getProjectConfig(); if (projectConfig) { processor.setProjectConfig(projectConfig); } diff --git a/koala-wrapper/native/src/bridges.cc b/koala-wrapper/native/src/bridges.cc index 0936041a4..c37ef5cb8 100644 --- a/koala-wrapper/native/src/bridges.cc +++ b/koala-wrapper/native/src/bridges.cc @@ -317,6 +317,29 @@ static KNativePointer impl_ExternalSourcePrograms(KNativePointer instance) } KOALA_INTEROP_1(ExternalSourcePrograms, KNativePointer, KNativePointer); +KNativePointer impl_CreateContextGenerateAbcForExternalSourceFiles( + KNativePointer configPtr, KInt fileNamesCount, KStringArray fileNames) +{ + auto config = reinterpret_cast(configPtr); + const std::size_t headerLen = 4; + const char **argv = + new const char *[static_cast(fileNamesCount)]; + std::size_t position = headerLen; + std::size_t strLen; + for (std::size_t i = 0; i < static_cast(fileNamesCount); ++i) { + strLen = unpackUInt(fileNames + position); + position += headerLen; + argv[i] = strdup(std::string(reinterpret_cast(fileNames + position), + strLen).c_str()); + position += strLen; + } + auto context = GetImpl()->CreateContextGenerateAbcForExternalSourceFiles( + config, fileNamesCount, argv); + delete[] argv; + return context; +} +KOALA_INTEROP_3(CreateContextGenerateAbcForExternalSourceFiles, KNativePointer, KNativePointer, KInt, KStringArray) + KBoolean impl_IsClassProperty(KNativePointer nodePtr) { auto node = reinterpret_cast(nodePtr); diff --git a/koala-wrapper/src/Es2pandaNativeModule.ts b/koala-wrapper/src/Es2pandaNativeModule.ts index 9435c1a3f..59666f9ca 100644 --- a/koala-wrapper/src/Es2pandaNativeModule.ts +++ b/koala-wrapper/src/Es2pandaNativeModule.ts @@ -105,6 +105,10 @@ export class Es2pandaNativeModule { _CreateContextFromString(config: KPtr, source: String, filename: String): KPtr { throw new Error('Not implemented'); } + _CreateContextGenerateAbcForExternalSourceFiles(config: KPtr, fileCount: KInt, filenames: + string[]): KPtr { + throw new Error('Not implemented'); + } _CreateContextFromFile(config: KPtr, filename: String): KPtr { throw new Error('Not implemented'); } diff --git a/koala-wrapper/src/arkts-api/peers/Context.ts b/koala-wrapper/src/arkts-api/peers/Context.ts index b4bf7a203..d091e7fd3 100644 --- a/koala-wrapper/src/arkts-api/peers/Context.ts +++ b/koala-wrapper/src/arkts-api/peers/Context.ts @@ -16,8 +16,8 @@ import { ArktsObject } from './ArktsObject'; import { global } from '../static/global'; import { throwError, filterSource } from '../../utils'; -import { passString } from '../utilities/private'; -import { KBoolean, KNativePointer } from '@koalaui/interop'; +import { passString, passStringArray } from '../utilities/private'; +import { KBoolean, KInt, KNativePointer } from '@koalaui/interop'; import { AstNode } from './AstNode'; import { Program } from './Program'; export class Context extends ArktsObject { @@ -45,6 +45,17 @@ export class Context extends ArktsObject { ); } + static createContextGenerateAbcForExternalSourceFiles( + filenames: string[]): Context { + if (!global.configIsInitialized()) { + throwError(`Config not initialized`); + } + return new Context( + global.es2panda._CreateContextGenerateAbcForExternalSourceFiles(global.config, filenames.length, passStringArray(filenames)) + ); + } + + static destroyAndRecreate(ast: AstNode): Context { console.log('[TS WRAPPER] DESTROY AND RECREATE'); const source = filterSource(ast.dumpSrc()); diff --git a/koala-wrapper/src/arkts-api/utilities/public.ts b/koala-wrapper/src/arkts-api/utilities/public.ts index 927073e74..faa8e3624 100644 --- a/koala-wrapper/src/arkts-api/utilities/public.ts +++ b/koala-wrapper/src/arkts-api/utilities/public.ts @@ -15,7 +15,7 @@ import { global } from '../static/global'; import { isNumber, throwError, getEnumName } from '../../utils'; -import { KNativePointer, KInt, nullptr, withStringResult } from '@koalaui/interop'; +import { KNativePointer, KInt, nullptr, withStringResult, KStringArrayPtr } from '@koalaui/interop' import { passNode, passString, passStringArray, unpackNodeArray, unpackNonNullableNode } from './private'; import { isFunctionDeclaration, isMemberExpression, isMethodDefinition, isNumberLiteral } from '../factory/nodeTests'; import { -- Gitee