@fictjs/compiler 0.0.4 → 0.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.cjs +244 -12
  2. package/dist/index.js +244 -12
  3. package/package.json +2 -2
package/dist/index.cjs CHANGED
@@ -14140,6 +14140,7 @@ var RUNTIME_HELPERS = {
14140
14140
  propsRest: "__fictPropsRest",
14141
14141
  mergeProps: "mergeProps",
14142
14142
  useProp: "useProp",
14143
+ runInScope: "runInScope",
14143
14144
  createElement: "createElement",
14144
14145
  conditional: "createConditional",
14145
14146
  list: "createList",
@@ -14180,6 +14181,7 @@ var RUNTIME_ALIASES = {
14180
14181
  propsRest: "__fictPropsRest",
14181
14182
  useProp: "useProp",
14182
14183
  mergeProps: "mergeProps",
14184
+ runInScope: "runInScope",
14183
14185
  createElement: "createElement",
14184
14186
  conditional: "createConditional",
14185
14187
  list: "createList",
@@ -15218,7 +15220,8 @@ function processStatement(stmt, bb, jumpTarget, ctx) {
15218
15220
  push({
15219
15221
  kind: "Assign",
15220
15222
  target: { kind: "Identifier", name: stmt.id.name },
15221
- value: convertExpression(fnExpr)
15223
+ value: convertExpression(fnExpr),
15224
+ declarationKind: "function"
15222
15225
  });
15223
15226
  return bb;
15224
15227
  }
@@ -17707,6 +17710,110 @@ function structurizeTry(ctx, block, term) {
17707
17710
  }
17708
17711
 
17709
17712
  // src/ir/regions.ts
17713
+ var REACTIVE_CREATORS = /* @__PURE__ */ new Set(["createEffect", "createMemo", "createSelector"]);
17714
+ function expressionCreatesReactive(expr) {
17715
+ if (expr.kind === "CallExpression" && expr.callee.kind === "Identifier") {
17716
+ const base = getSSABaseName(expr.callee.name);
17717
+ return REACTIVE_CREATORS.has(base);
17718
+ }
17719
+ return false;
17720
+ }
17721
+ function expressionContainsReactiveCreation(expr) {
17722
+ if (expressionCreatesReactive(expr)) return true;
17723
+ switch (expr.kind) {
17724
+ case "CallExpression":
17725
+ return expressionContainsReactiveCreation(expr.callee) || expr.arguments.some((arg) => expressionContainsReactiveCreation(arg));
17726
+ case "MemberExpression":
17727
+ return expressionContainsReactiveCreation(expr.object) || expressionContainsReactiveCreation(expr.property);
17728
+ case "BinaryExpression":
17729
+ case "LogicalExpression":
17730
+ return expressionContainsReactiveCreation(expr.left) || expressionContainsReactiveCreation(expr.right);
17731
+ case "UnaryExpression":
17732
+ return expressionContainsReactiveCreation(expr.argument);
17733
+ case "ConditionalExpression":
17734
+ return expressionContainsReactiveCreation(expr.test) || expressionContainsReactiveCreation(expr.consequent) || expressionContainsReactiveCreation(expr.alternate);
17735
+ case "ArrayExpression":
17736
+ return expr.elements.some((el) => el && expressionContainsReactiveCreation(el));
17737
+ case "ObjectExpression":
17738
+ return expr.properties.some(
17739
+ (prop) => prop.kind === "SpreadElement" ? expressionContainsReactiveCreation(prop.argument) : expressionContainsReactiveCreation(prop.value)
17740
+ );
17741
+ case "ArrowFunction":
17742
+ if (expr.isExpression) {
17743
+ return expressionContainsReactiveCreation(expr.body);
17744
+ }
17745
+ return Array.isArray(expr.body) ? expr.body.some(
17746
+ (block) => block.instructions.some((i) => instructionContainsReactiveCreation(i))
17747
+ ) : false;
17748
+ case "FunctionExpression":
17749
+ return expr.body.some(
17750
+ (block) => block.instructions.some((i) => instructionContainsReactiveCreation(i))
17751
+ );
17752
+ case "AssignmentExpression":
17753
+ return expressionContainsReactiveCreation(expr.left) || expressionContainsReactiveCreation(expr.right);
17754
+ case "UpdateExpression":
17755
+ return expressionContainsReactiveCreation(expr.argument);
17756
+ case "TemplateLiteral":
17757
+ return expr.expressions.some((e) => expressionContainsReactiveCreation(e));
17758
+ case "SpreadElement":
17759
+ return expressionContainsReactiveCreation(expr.argument);
17760
+ case "AwaitExpression":
17761
+ return expressionContainsReactiveCreation(expr.argument);
17762
+ case "YieldExpression":
17763
+ return expr.argument ? expressionContainsReactiveCreation(expr.argument) : false;
17764
+ case "NewExpression":
17765
+ return expressionContainsReactiveCreation(expr.callee) || expr.arguments.some((arg) => expressionContainsReactiveCreation(arg));
17766
+ case "OptionalCallExpression":
17767
+ return expressionContainsReactiveCreation(expr.callee) || expr.arguments.some((arg) => expressionContainsReactiveCreation(arg));
17768
+ case "JSXElement":
17769
+ return typeof expr.tagName !== "string" && expressionContainsReactiveCreation(expr.tagName) || expr.attributes.some(
17770
+ (attr) => attr.isSpread ? !!attr.spreadExpr && expressionContainsReactiveCreation(attr.spreadExpr) : attr.value ? expressionContainsReactiveCreation(attr.value) : false
17771
+ ) || expr.children.some(
17772
+ (child) => child.kind === "expression" ? expressionContainsReactiveCreation(child.value) : false
17773
+ );
17774
+ default:
17775
+ return false;
17776
+ }
17777
+ }
17778
+ function instructionContainsReactiveCreation(instr) {
17779
+ if (instr.kind === "Assign") {
17780
+ return expressionContainsReactiveCreation(instr.value);
17781
+ }
17782
+ if (instr.kind === "Expression") {
17783
+ return expressionContainsReactiveCreation(instr.value);
17784
+ }
17785
+ return false;
17786
+ }
17787
+ function instructionIsReactiveSetup(instr) {
17788
+ if (instr.kind === "Assign") {
17789
+ return expressionCreatesReactive(instr.value);
17790
+ }
17791
+ if (instr.kind === "Expression") {
17792
+ return expressionCreatesReactive(instr.value);
17793
+ }
17794
+ return false;
17795
+ }
17796
+ function nodeIsPureReactiveScope(node) {
17797
+ let found = false;
17798
+ const visit = (n) => {
17799
+ switch (n.kind) {
17800
+ case "instruction": {
17801
+ const ok = instructionIsReactiveSetup(n.instruction);
17802
+ if (ok && instructionContainsReactiveCreation(n.instruction)) found = true;
17803
+ return ok;
17804
+ }
17805
+ case "sequence":
17806
+ if (n.nodes.length === 0) return false;
17807
+ return n.nodes.every((child) => visit(child));
17808
+ case "block":
17809
+ if (n.statements.length === 0) return false;
17810
+ return n.statements.every((child) => visit(child));
17811
+ default:
17812
+ return false;
17813
+ }
17814
+ };
17815
+ return visit(node) && found;
17816
+ }
17710
17817
  function generateRegions(fn, scopeResult, shapeResult = analyzeObjectShapes(fn)) {
17711
17818
  const regions = [];
17712
17819
  const regionsByBlock = /* @__PURE__ */ new Map();
@@ -18047,14 +18154,67 @@ function lowerNodeWithRegionContext(node, t2, ctx, declaredVars, regionCtx) {
18047
18154
  case "if": {
18048
18155
  const prevConditional = ctx.inConditional ?? 0;
18049
18156
  ctx.inConditional = prevConditional + 1;
18050
- const conseq = t2.blockStatement(
18051
- lowerNodeWithRegionContext(node.consequent, t2, ctx, declaredVars, regionCtx)
18157
+ const conseqStmts = lowerNodeWithRegionContext(
18158
+ node.consequent,
18159
+ t2,
18160
+ ctx,
18161
+ declaredVars,
18162
+ regionCtx
18052
18163
  );
18053
- const alt = node.alternate ? t2.blockStatement(
18054
- lowerNodeWithRegionContext(node.alternate, t2, ctx, declaredVars, regionCtx)
18055
- ) : null;
18164
+ const altStmts = node.alternate ? lowerNodeWithRegionContext(node.alternate, t2, ctx, declaredVars, regionCtx) : null;
18056
18165
  ctx.inConditional = prevConditional;
18057
- const ifStmt = t2.ifStatement(lowerExpressionWithDeSSA(node.test, ctx), conseq, alt);
18166
+ const conseqReactiveOnly = nodeIsPureReactiveScope(node.consequent);
18167
+ const altReactiveOnly = node.alternate ? nodeIsPureReactiveScope(node.alternate) : false;
18168
+ const testExpr = lowerExpressionWithDeSSA(node.test, ctx);
18169
+ const unwrapTestExpr = () => {
18170
+ if (t2.isArrowFunctionExpression(testExpr) && testExpr.params.length === 0 && !t2.isBlockStatement(testExpr.body)) {
18171
+ return t2.cloneNode(testExpr.body);
18172
+ }
18173
+ return t2.cloneNode(testExpr);
18174
+ };
18175
+ const createFlagExpr = (negate = false) => {
18176
+ const body = unwrapTestExpr();
18177
+ const bodyExpr = negate ? t2.unaryExpression("!", body) : body;
18178
+ return t2.arrowFunctionExpression([], bodyExpr);
18179
+ };
18180
+ if (conseqReactiveOnly || altReactiveOnly) {
18181
+ const stmts = [];
18182
+ const runInScopeId = t2.identifier(RUNTIME_ALIASES.runInScope);
18183
+ const addScoped = (flagExpr, body) => {
18184
+ ctx.helpersUsed.add("runInScope");
18185
+ stmts.push(
18186
+ t2.expressionStatement(
18187
+ t2.callExpression(runInScopeId, [
18188
+ flagExpr,
18189
+ t2.arrowFunctionExpression([], t2.blockStatement(body))
18190
+ ])
18191
+ )
18192
+ );
18193
+ };
18194
+ if (conseqReactiveOnly) {
18195
+ addScoped(createFlagExpr(false), conseqStmts);
18196
+ }
18197
+ if (altReactiveOnly && altStmts) {
18198
+ addScoped(createFlagExpr(true), altStmts);
18199
+ }
18200
+ const needsFallbackConseq = !conseqReactiveOnly && conseqStmts.length > 0;
18201
+ const needsFallbackAlt = !altReactiveOnly && altStmts && altStmts.length > 0;
18202
+ if (needsFallbackConseq || needsFallbackAlt) {
18203
+ stmts.push(
18204
+ t2.ifStatement(
18205
+ unwrapTestExpr(),
18206
+ needsFallbackConseq ? t2.blockStatement(conseqStmts) : t2.blockStatement([]),
18207
+ needsFallbackAlt && altStmts ? t2.blockStatement(altStmts) : null
18208
+ )
18209
+ );
18210
+ }
18211
+ return stmts;
18212
+ }
18213
+ const ifStmt = t2.ifStatement(
18214
+ testExpr,
18215
+ t2.blockStatement(conseqStmts),
18216
+ altStmts ? t2.blockStatement(altStmts) : null
18217
+ );
18058
18218
  const shouldWrapEffect = ctx.wrapTrackedExpressions !== false && !ctx.inRegionMemo && expressionUsesTracked(node.test, ctx) && !statementHasEarlyExit(ifStmt, t2);
18059
18219
  if (shouldWrapEffect) {
18060
18220
  ctx.helpersUsed.add("useEffect");
@@ -19048,6 +19208,7 @@ function instructionToStatement(instr, t2, declaredVars, ctx, _buildMemoCall) {
19048
19208
  if (instr.kind === "Assign") {
19049
19209
  const ssaName = instr.target.name;
19050
19210
  const baseName2 = deSSAVarName(ssaName);
19211
+ const declKindRaw = instr.declarationKind;
19051
19212
  propagateHookResultAlias(baseName2, instr.value, ctx);
19052
19213
  const hookMember = resolveHookMemberValue(instr.value, ctx);
19053
19214
  if (hookMember) {
@@ -19057,9 +19218,10 @@ function instructionToStatement(instr, t2, declaredVars, ctx, _buildMemoCall) {
19057
19218
  } else if (hookMember.kind === "memo") {
19058
19219
  ctx.memoVars?.add(baseName2);
19059
19220
  }
19060
- if (instr.declarationKind) {
19221
+ const declKind2 = declKindRaw && declKindRaw !== "function" ? declKindRaw : null;
19222
+ if (declKind2) {
19061
19223
  declaredVars.add(baseName2);
19062
- return t2.variableDeclaration(instr.declarationKind, [
19224
+ return t2.variableDeclaration(declKind2, [
19063
19225
  t2.variableDeclarator(t2.identifier(baseName2), hookMember.member)
19064
19226
  ]);
19065
19227
  }
@@ -19072,7 +19234,21 @@ function instructionToStatement(instr, t2, declaredVars, ctx, _buildMemoCall) {
19072
19234
  t2.assignmentExpression("=", t2.identifier(baseName2), hookMember.member)
19073
19235
  );
19074
19236
  }
19075
- const declKind = instr.declarationKind;
19237
+ const declKind = declKindRaw && declKindRaw !== "function" ? declKindRaw : void 0;
19238
+ const isFunctionDecl = instr.value.kind === "FunctionExpression" && (declKindRaw === "function" || !declKindRaw && instr.value.name === baseName2);
19239
+ if (isFunctionDecl) {
19240
+ const loweredFn = lowerExpressionWithDeSSA(instr.value, ctx);
19241
+ if (t2.isFunctionExpression(loweredFn)) {
19242
+ declaredVars.add(baseName2);
19243
+ return t2.functionDeclaration(
19244
+ t2.identifier(baseName2),
19245
+ loweredFn.params,
19246
+ loweredFn.body,
19247
+ loweredFn.generator ?? false,
19248
+ loweredFn.async ?? false
19249
+ );
19250
+ }
19251
+ }
19076
19252
  const isTracked = ctx.trackedVars.has(baseName2);
19077
19253
  const isSignal = ctx.signalVars?.has(baseName2) ?? false;
19078
19254
  const aliasVars = ctx.aliasVars ?? (ctx.aliasVars = /* @__PURE__ */ new Set());
@@ -20821,6 +20997,20 @@ function lowerInstruction(instr, ctx) {
20821
20997
  const { t: t2 } = ctx;
20822
20998
  if (instr.kind === "Assign") {
20823
20999
  const baseName2 = deSSAVarName(instr.target.name);
21000
+ const isFunctionDecl = instr.value.kind === "FunctionExpression" && (instr.declarationKind === "function" || !instr.declarationKind && instr.value.name === baseName2);
21001
+ if (isFunctionDecl) {
21002
+ const loweredFn = lowerExpression(instr.value, ctx);
21003
+ if (t2.isFunctionExpression(loweredFn)) {
21004
+ return t2.functionDeclaration(
21005
+ t2.identifier(baseName2),
21006
+ loweredFn.params,
21007
+ loweredFn.body,
21008
+ loweredFn.generator ?? false,
21009
+ loweredFn.async ?? false
21010
+ );
21011
+ }
21012
+ }
21013
+ const declKind = instr.declarationKind === "function" ? void 0 : instr.declarationKind;
20824
21014
  propagateHookResultAlias(baseName2, instr.value, ctx);
20825
21015
  const hookMember = resolveHookMemberValue(instr.value, ctx);
20826
21016
  if (hookMember) {
@@ -20830,8 +21020,8 @@ function lowerInstruction(instr, ctx) {
20830
21020
  } else if (hookMember.kind === "memo") {
20831
21021
  ctx.memoVars?.add(baseName2);
20832
21022
  }
20833
- if (instr.declarationKind) {
20834
- return t2.variableDeclaration(instr.declarationKind, [
21023
+ if (declKind) {
21024
+ return t2.variableDeclaration(declKind, [
20835
21025
  t2.variableDeclarator(t2.identifier(baseName2), hookMember.member)
20836
21026
  ]);
20837
21027
  }
@@ -24134,6 +24324,9 @@ function isInsideNestedFunction(path) {
24134
24324
  }
24135
24325
  return false;
24136
24326
  }
24327
+ function isInsideJSX(path) {
24328
+ return !!path.findParent((p) => p.isJSXElement?.() || p.isJSXFragment?.());
24329
+ }
24137
24330
  function emitWarning(node, code, message, options, fileName) {
24138
24331
  if (!options.onWarn) return;
24139
24332
  const loc = node.loc?.start;
@@ -24231,6 +24424,7 @@ function runWarningPass(programPath, stateVars, derivedVars, options, t2) {
24231
24424
  const root = getRootIdentifier(expr, t2);
24232
24425
  return !!(root && stateVars.has(root.name));
24233
24426
  };
24427
+ const reactiveNames = /* @__PURE__ */ new Set([...stateVars, ...derivedVars]);
24234
24428
  programPath.traverse({
24235
24429
  AssignmentExpression(path) {
24236
24430
  const { left } = path.node;
@@ -24293,6 +24487,35 @@ function runWarningPass(programPath, stateVars, derivedVars, options, t2) {
24293
24487
  );
24294
24488
  }
24295
24489
  },
24490
+ Function(path) {
24491
+ const captured = /* @__PURE__ */ new Set();
24492
+ path.traverse(
24493
+ {
24494
+ Function(inner) {
24495
+ if (inner === path) return;
24496
+ inner.skip();
24497
+ },
24498
+ Identifier(idPath) {
24499
+ const name = idPath.node.name;
24500
+ if (!reactiveNames.has(name)) return;
24501
+ const binding = idPath.scope.getBinding(name);
24502
+ if (!binding) return;
24503
+ if (binding.scope === idPath.scope || binding.scope === path.scope) return;
24504
+ captured.add(name);
24505
+ }
24506
+ },
24507
+ {}
24508
+ );
24509
+ if (captured.size > 0) {
24510
+ emitWarning(
24511
+ path.node,
24512
+ "FICT-R005",
24513
+ `Function captures reactive variable(s): ${Array.from(captured).join(", ")}. Pass them as parameters or memoize explicitly to avoid hidden dependencies.`,
24514
+ options,
24515
+ fileName
24516
+ );
24517
+ }
24518
+ },
24296
24519
  CallExpression(path) {
24297
24520
  if (t2.isIdentifier(path.node.callee, { name: "$effect" })) {
24298
24521
  const argPath = path.get("arguments.0");
@@ -24614,6 +24837,15 @@ function createHIREntrypointVisitor(t2, options) {
24614
24837
  }
24615
24838
  const callee = callPath.node.callee;
24616
24839
  const calleeId = t2.isIdentifier(callee) ? callee.name : null;
24840
+ if (calleeId && (calleeId === "createEffect" || calleeId === "createMemo" || calleeId === "createSelector") && fictImports.has(calleeId) && (isInsideLoop(callPath) || isInsideConditional(callPath)) && !isInsideJSX(callPath)) {
24841
+ emitWarning(
24842
+ callPath.node,
24843
+ "FICT-R004",
24844
+ "Reactive creation inside non-JSX control flow will not auto-dispose; wrap it in createScope/runInScope or move it into JSX-managed regions.",
24845
+ options,
24846
+ fileName
24847
+ );
24848
+ }
24617
24849
  const allowedStateCallees = /* @__PURE__ */ new Set([
24618
24850
  "$effect",
24619
24851
  "$memo",
package/dist/index.js CHANGED
@@ -14128,6 +14128,7 @@ var RUNTIME_HELPERS = {
14128
14128
  propsRest: "__fictPropsRest",
14129
14129
  mergeProps: "mergeProps",
14130
14130
  useProp: "useProp",
14131
+ runInScope: "runInScope",
14131
14132
  createElement: "createElement",
14132
14133
  conditional: "createConditional",
14133
14134
  list: "createList",
@@ -14168,6 +14169,7 @@ var RUNTIME_ALIASES = {
14168
14169
  propsRest: "__fictPropsRest",
14169
14170
  useProp: "useProp",
14170
14171
  mergeProps: "mergeProps",
14172
+ runInScope: "runInScope",
14171
14173
  createElement: "createElement",
14172
14174
  conditional: "createConditional",
14173
14175
  list: "createList",
@@ -15206,7 +15208,8 @@ function processStatement(stmt, bb, jumpTarget, ctx) {
15206
15208
  push({
15207
15209
  kind: "Assign",
15208
15210
  target: { kind: "Identifier", name: stmt.id.name },
15209
- value: convertExpression(fnExpr)
15211
+ value: convertExpression(fnExpr),
15212
+ declarationKind: "function"
15210
15213
  });
15211
15214
  return bb;
15212
15215
  }
@@ -17695,6 +17698,110 @@ function structurizeTry(ctx, block, term) {
17695
17698
  }
17696
17699
 
17697
17700
  // src/ir/regions.ts
17701
+ var REACTIVE_CREATORS = /* @__PURE__ */ new Set(["createEffect", "createMemo", "createSelector"]);
17702
+ function expressionCreatesReactive(expr) {
17703
+ if (expr.kind === "CallExpression" && expr.callee.kind === "Identifier") {
17704
+ const base = getSSABaseName(expr.callee.name);
17705
+ return REACTIVE_CREATORS.has(base);
17706
+ }
17707
+ return false;
17708
+ }
17709
+ function expressionContainsReactiveCreation(expr) {
17710
+ if (expressionCreatesReactive(expr)) return true;
17711
+ switch (expr.kind) {
17712
+ case "CallExpression":
17713
+ return expressionContainsReactiveCreation(expr.callee) || expr.arguments.some((arg) => expressionContainsReactiveCreation(arg));
17714
+ case "MemberExpression":
17715
+ return expressionContainsReactiveCreation(expr.object) || expressionContainsReactiveCreation(expr.property);
17716
+ case "BinaryExpression":
17717
+ case "LogicalExpression":
17718
+ return expressionContainsReactiveCreation(expr.left) || expressionContainsReactiveCreation(expr.right);
17719
+ case "UnaryExpression":
17720
+ return expressionContainsReactiveCreation(expr.argument);
17721
+ case "ConditionalExpression":
17722
+ return expressionContainsReactiveCreation(expr.test) || expressionContainsReactiveCreation(expr.consequent) || expressionContainsReactiveCreation(expr.alternate);
17723
+ case "ArrayExpression":
17724
+ return expr.elements.some((el) => el && expressionContainsReactiveCreation(el));
17725
+ case "ObjectExpression":
17726
+ return expr.properties.some(
17727
+ (prop) => prop.kind === "SpreadElement" ? expressionContainsReactiveCreation(prop.argument) : expressionContainsReactiveCreation(prop.value)
17728
+ );
17729
+ case "ArrowFunction":
17730
+ if (expr.isExpression) {
17731
+ return expressionContainsReactiveCreation(expr.body);
17732
+ }
17733
+ return Array.isArray(expr.body) ? expr.body.some(
17734
+ (block) => block.instructions.some((i) => instructionContainsReactiveCreation(i))
17735
+ ) : false;
17736
+ case "FunctionExpression":
17737
+ return expr.body.some(
17738
+ (block) => block.instructions.some((i) => instructionContainsReactiveCreation(i))
17739
+ );
17740
+ case "AssignmentExpression":
17741
+ return expressionContainsReactiveCreation(expr.left) || expressionContainsReactiveCreation(expr.right);
17742
+ case "UpdateExpression":
17743
+ return expressionContainsReactiveCreation(expr.argument);
17744
+ case "TemplateLiteral":
17745
+ return expr.expressions.some((e) => expressionContainsReactiveCreation(e));
17746
+ case "SpreadElement":
17747
+ return expressionContainsReactiveCreation(expr.argument);
17748
+ case "AwaitExpression":
17749
+ return expressionContainsReactiveCreation(expr.argument);
17750
+ case "YieldExpression":
17751
+ return expr.argument ? expressionContainsReactiveCreation(expr.argument) : false;
17752
+ case "NewExpression":
17753
+ return expressionContainsReactiveCreation(expr.callee) || expr.arguments.some((arg) => expressionContainsReactiveCreation(arg));
17754
+ case "OptionalCallExpression":
17755
+ return expressionContainsReactiveCreation(expr.callee) || expr.arguments.some((arg) => expressionContainsReactiveCreation(arg));
17756
+ case "JSXElement":
17757
+ return typeof expr.tagName !== "string" && expressionContainsReactiveCreation(expr.tagName) || expr.attributes.some(
17758
+ (attr) => attr.isSpread ? !!attr.spreadExpr && expressionContainsReactiveCreation(attr.spreadExpr) : attr.value ? expressionContainsReactiveCreation(attr.value) : false
17759
+ ) || expr.children.some(
17760
+ (child) => child.kind === "expression" ? expressionContainsReactiveCreation(child.value) : false
17761
+ );
17762
+ default:
17763
+ return false;
17764
+ }
17765
+ }
17766
+ function instructionContainsReactiveCreation(instr) {
17767
+ if (instr.kind === "Assign") {
17768
+ return expressionContainsReactiveCreation(instr.value);
17769
+ }
17770
+ if (instr.kind === "Expression") {
17771
+ return expressionContainsReactiveCreation(instr.value);
17772
+ }
17773
+ return false;
17774
+ }
17775
+ function instructionIsReactiveSetup(instr) {
17776
+ if (instr.kind === "Assign") {
17777
+ return expressionCreatesReactive(instr.value);
17778
+ }
17779
+ if (instr.kind === "Expression") {
17780
+ return expressionCreatesReactive(instr.value);
17781
+ }
17782
+ return false;
17783
+ }
17784
+ function nodeIsPureReactiveScope(node) {
17785
+ let found = false;
17786
+ const visit = (n) => {
17787
+ switch (n.kind) {
17788
+ case "instruction": {
17789
+ const ok = instructionIsReactiveSetup(n.instruction);
17790
+ if (ok && instructionContainsReactiveCreation(n.instruction)) found = true;
17791
+ return ok;
17792
+ }
17793
+ case "sequence":
17794
+ if (n.nodes.length === 0) return false;
17795
+ return n.nodes.every((child) => visit(child));
17796
+ case "block":
17797
+ if (n.statements.length === 0) return false;
17798
+ return n.statements.every((child) => visit(child));
17799
+ default:
17800
+ return false;
17801
+ }
17802
+ };
17803
+ return visit(node) && found;
17804
+ }
17698
17805
  function generateRegions(fn, scopeResult, shapeResult = analyzeObjectShapes(fn)) {
17699
17806
  const regions = [];
17700
17807
  const regionsByBlock = /* @__PURE__ */ new Map();
@@ -18035,14 +18142,67 @@ function lowerNodeWithRegionContext(node, t2, ctx, declaredVars, regionCtx) {
18035
18142
  case "if": {
18036
18143
  const prevConditional = ctx.inConditional ?? 0;
18037
18144
  ctx.inConditional = prevConditional + 1;
18038
- const conseq = t2.blockStatement(
18039
- lowerNodeWithRegionContext(node.consequent, t2, ctx, declaredVars, regionCtx)
18145
+ const conseqStmts = lowerNodeWithRegionContext(
18146
+ node.consequent,
18147
+ t2,
18148
+ ctx,
18149
+ declaredVars,
18150
+ regionCtx
18040
18151
  );
18041
- const alt = node.alternate ? t2.blockStatement(
18042
- lowerNodeWithRegionContext(node.alternate, t2, ctx, declaredVars, regionCtx)
18043
- ) : null;
18152
+ const altStmts = node.alternate ? lowerNodeWithRegionContext(node.alternate, t2, ctx, declaredVars, regionCtx) : null;
18044
18153
  ctx.inConditional = prevConditional;
18045
- const ifStmt = t2.ifStatement(lowerExpressionWithDeSSA(node.test, ctx), conseq, alt);
18154
+ const conseqReactiveOnly = nodeIsPureReactiveScope(node.consequent);
18155
+ const altReactiveOnly = node.alternate ? nodeIsPureReactiveScope(node.alternate) : false;
18156
+ const testExpr = lowerExpressionWithDeSSA(node.test, ctx);
18157
+ const unwrapTestExpr = () => {
18158
+ if (t2.isArrowFunctionExpression(testExpr) && testExpr.params.length === 0 && !t2.isBlockStatement(testExpr.body)) {
18159
+ return t2.cloneNode(testExpr.body);
18160
+ }
18161
+ return t2.cloneNode(testExpr);
18162
+ };
18163
+ const createFlagExpr = (negate = false) => {
18164
+ const body = unwrapTestExpr();
18165
+ const bodyExpr = negate ? t2.unaryExpression("!", body) : body;
18166
+ return t2.arrowFunctionExpression([], bodyExpr);
18167
+ };
18168
+ if (conseqReactiveOnly || altReactiveOnly) {
18169
+ const stmts = [];
18170
+ const runInScopeId = t2.identifier(RUNTIME_ALIASES.runInScope);
18171
+ const addScoped = (flagExpr, body) => {
18172
+ ctx.helpersUsed.add("runInScope");
18173
+ stmts.push(
18174
+ t2.expressionStatement(
18175
+ t2.callExpression(runInScopeId, [
18176
+ flagExpr,
18177
+ t2.arrowFunctionExpression([], t2.blockStatement(body))
18178
+ ])
18179
+ )
18180
+ );
18181
+ };
18182
+ if (conseqReactiveOnly) {
18183
+ addScoped(createFlagExpr(false), conseqStmts);
18184
+ }
18185
+ if (altReactiveOnly && altStmts) {
18186
+ addScoped(createFlagExpr(true), altStmts);
18187
+ }
18188
+ const needsFallbackConseq = !conseqReactiveOnly && conseqStmts.length > 0;
18189
+ const needsFallbackAlt = !altReactiveOnly && altStmts && altStmts.length > 0;
18190
+ if (needsFallbackConseq || needsFallbackAlt) {
18191
+ stmts.push(
18192
+ t2.ifStatement(
18193
+ unwrapTestExpr(),
18194
+ needsFallbackConseq ? t2.blockStatement(conseqStmts) : t2.blockStatement([]),
18195
+ needsFallbackAlt && altStmts ? t2.blockStatement(altStmts) : null
18196
+ )
18197
+ );
18198
+ }
18199
+ return stmts;
18200
+ }
18201
+ const ifStmt = t2.ifStatement(
18202
+ testExpr,
18203
+ t2.blockStatement(conseqStmts),
18204
+ altStmts ? t2.blockStatement(altStmts) : null
18205
+ );
18046
18206
  const shouldWrapEffect = ctx.wrapTrackedExpressions !== false && !ctx.inRegionMemo && expressionUsesTracked(node.test, ctx) && !statementHasEarlyExit(ifStmt, t2);
18047
18207
  if (shouldWrapEffect) {
18048
18208
  ctx.helpersUsed.add("useEffect");
@@ -19036,6 +19196,7 @@ function instructionToStatement(instr, t2, declaredVars, ctx, _buildMemoCall) {
19036
19196
  if (instr.kind === "Assign") {
19037
19197
  const ssaName = instr.target.name;
19038
19198
  const baseName2 = deSSAVarName(ssaName);
19199
+ const declKindRaw = instr.declarationKind;
19039
19200
  propagateHookResultAlias(baseName2, instr.value, ctx);
19040
19201
  const hookMember = resolveHookMemberValue(instr.value, ctx);
19041
19202
  if (hookMember) {
@@ -19045,9 +19206,10 @@ function instructionToStatement(instr, t2, declaredVars, ctx, _buildMemoCall) {
19045
19206
  } else if (hookMember.kind === "memo") {
19046
19207
  ctx.memoVars?.add(baseName2);
19047
19208
  }
19048
- if (instr.declarationKind) {
19209
+ const declKind2 = declKindRaw && declKindRaw !== "function" ? declKindRaw : null;
19210
+ if (declKind2) {
19049
19211
  declaredVars.add(baseName2);
19050
- return t2.variableDeclaration(instr.declarationKind, [
19212
+ return t2.variableDeclaration(declKind2, [
19051
19213
  t2.variableDeclarator(t2.identifier(baseName2), hookMember.member)
19052
19214
  ]);
19053
19215
  }
@@ -19060,7 +19222,21 @@ function instructionToStatement(instr, t2, declaredVars, ctx, _buildMemoCall) {
19060
19222
  t2.assignmentExpression("=", t2.identifier(baseName2), hookMember.member)
19061
19223
  );
19062
19224
  }
19063
- const declKind = instr.declarationKind;
19225
+ const declKind = declKindRaw && declKindRaw !== "function" ? declKindRaw : void 0;
19226
+ const isFunctionDecl = instr.value.kind === "FunctionExpression" && (declKindRaw === "function" || !declKindRaw && instr.value.name === baseName2);
19227
+ if (isFunctionDecl) {
19228
+ const loweredFn = lowerExpressionWithDeSSA(instr.value, ctx);
19229
+ if (t2.isFunctionExpression(loweredFn)) {
19230
+ declaredVars.add(baseName2);
19231
+ return t2.functionDeclaration(
19232
+ t2.identifier(baseName2),
19233
+ loweredFn.params,
19234
+ loweredFn.body,
19235
+ loweredFn.generator ?? false,
19236
+ loweredFn.async ?? false
19237
+ );
19238
+ }
19239
+ }
19064
19240
  const isTracked = ctx.trackedVars.has(baseName2);
19065
19241
  const isSignal = ctx.signalVars?.has(baseName2) ?? false;
19066
19242
  const aliasVars = ctx.aliasVars ?? (ctx.aliasVars = /* @__PURE__ */ new Set());
@@ -20809,6 +20985,20 @@ function lowerInstruction(instr, ctx) {
20809
20985
  const { t: t2 } = ctx;
20810
20986
  if (instr.kind === "Assign") {
20811
20987
  const baseName2 = deSSAVarName(instr.target.name);
20988
+ const isFunctionDecl = instr.value.kind === "FunctionExpression" && (instr.declarationKind === "function" || !instr.declarationKind && instr.value.name === baseName2);
20989
+ if (isFunctionDecl) {
20990
+ const loweredFn = lowerExpression(instr.value, ctx);
20991
+ if (t2.isFunctionExpression(loweredFn)) {
20992
+ return t2.functionDeclaration(
20993
+ t2.identifier(baseName2),
20994
+ loweredFn.params,
20995
+ loweredFn.body,
20996
+ loweredFn.generator ?? false,
20997
+ loweredFn.async ?? false
20998
+ );
20999
+ }
21000
+ }
21001
+ const declKind = instr.declarationKind === "function" ? void 0 : instr.declarationKind;
20812
21002
  propagateHookResultAlias(baseName2, instr.value, ctx);
20813
21003
  const hookMember = resolveHookMemberValue(instr.value, ctx);
20814
21004
  if (hookMember) {
@@ -20818,8 +21008,8 @@ function lowerInstruction(instr, ctx) {
20818
21008
  } else if (hookMember.kind === "memo") {
20819
21009
  ctx.memoVars?.add(baseName2);
20820
21010
  }
20821
- if (instr.declarationKind) {
20822
- return t2.variableDeclaration(instr.declarationKind, [
21011
+ if (declKind) {
21012
+ return t2.variableDeclaration(declKind, [
20823
21013
  t2.variableDeclarator(t2.identifier(baseName2), hookMember.member)
20824
21014
  ]);
20825
21015
  }
@@ -24122,6 +24312,9 @@ function isInsideNestedFunction(path) {
24122
24312
  }
24123
24313
  return false;
24124
24314
  }
24315
+ function isInsideJSX(path) {
24316
+ return !!path.findParent((p) => p.isJSXElement?.() || p.isJSXFragment?.());
24317
+ }
24125
24318
  function emitWarning(node, code, message, options, fileName) {
24126
24319
  if (!options.onWarn) return;
24127
24320
  const loc = node.loc?.start;
@@ -24219,6 +24412,7 @@ function runWarningPass(programPath, stateVars, derivedVars, options, t2) {
24219
24412
  const root = getRootIdentifier(expr, t2);
24220
24413
  return !!(root && stateVars.has(root.name));
24221
24414
  };
24415
+ const reactiveNames = /* @__PURE__ */ new Set([...stateVars, ...derivedVars]);
24222
24416
  programPath.traverse({
24223
24417
  AssignmentExpression(path) {
24224
24418
  const { left } = path.node;
@@ -24281,6 +24475,35 @@ function runWarningPass(programPath, stateVars, derivedVars, options, t2) {
24281
24475
  );
24282
24476
  }
24283
24477
  },
24478
+ Function(path) {
24479
+ const captured = /* @__PURE__ */ new Set();
24480
+ path.traverse(
24481
+ {
24482
+ Function(inner) {
24483
+ if (inner === path) return;
24484
+ inner.skip();
24485
+ },
24486
+ Identifier(idPath) {
24487
+ const name = idPath.node.name;
24488
+ if (!reactiveNames.has(name)) return;
24489
+ const binding = idPath.scope.getBinding(name);
24490
+ if (!binding) return;
24491
+ if (binding.scope === idPath.scope || binding.scope === path.scope) return;
24492
+ captured.add(name);
24493
+ }
24494
+ },
24495
+ {}
24496
+ );
24497
+ if (captured.size > 0) {
24498
+ emitWarning(
24499
+ path.node,
24500
+ "FICT-R005",
24501
+ `Function captures reactive variable(s): ${Array.from(captured).join(", ")}. Pass them as parameters or memoize explicitly to avoid hidden dependencies.`,
24502
+ options,
24503
+ fileName
24504
+ );
24505
+ }
24506
+ },
24284
24507
  CallExpression(path) {
24285
24508
  if (t2.isIdentifier(path.node.callee, { name: "$effect" })) {
24286
24509
  const argPath = path.get("arguments.0");
@@ -24602,6 +24825,15 @@ function createHIREntrypointVisitor(t2, options) {
24602
24825
  }
24603
24826
  const callee = callPath.node.callee;
24604
24827
  const calleeId = t2.isIdentifier(callee) ? callee.name : null;
24828
+ if (calleeId && (calleeId === "createEffect" || calleeId === "createMemo" || calleeId === "createSelector") && fictImports.has(calleeId) && (isInsideLoop(callPath) || isInsideConditional(callPath)) && !isInsideJSX(callPath)) {
24829
+ emitWarning(
24830
+ callPath.node,
24831
+ "FICT-R004",
24832
+ "Reactive creation inside non-JSX control flow will not auto-dispose; wrap it in createScope/runInScope or move it into JSX-managed regions.",
24833
+ options,
24834
+ fileName
24835
+ );
24836
+ }
24605
24837
  const allowedStateCallees = /* @__PURE__ */ new Set([
24606
24838
  "$effect",
24607
24839
  "$memo",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fictjs/compiler",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Babel plugin for Fict Compiler",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -38,7 +38,7 @@
38
38
  "@types/babel__helper-plugin-utils": "^7.10.3",
39
39
  "@types/babel__traverse": "^7.28.0",
40
40
  "tsup": "^8.5.1",
41
- "@fictjs/runtime": "0.0.4"
41
+ "@fictjs/runtime": "0.0.5"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsup src/index.ts --format cjs,esm --dts",