@vesk/compiler 0.2.55 → 0.2.56

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.
@@ -1 +1 @@
1
- {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA2T/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AAo+CD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAmV9E"}
1
+ {"version":3,"file":"ir-generator.d.ts","sourceRoot":"","sources":["../src/ir-generator.ts"],"names":[],"mappings":"AACA,OAAO,EACL,MAAM,EAsBP,MAAM,uBAAuB,CAAC;AA2T/B,wBAAgB,YAAY,CAAC,MAAM,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,IAAI,CAAC,EAAE,GAAG,CAAC;IAAC,cAAc,CAAC,EAAE,GAAG,CAAC;IAAC,UAAU,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,CAAC,GAAG,SAAS,GAAG,IAAI,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAkCnL;AA4lDD,wBAAgB,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAqV9E"}
@@ -1959,6 +1959,154 @@ function idxOfQuote(s, from) {
1959
1959
  }
1960
1960
  return -1;
1961
1961
  }
1962
+ /**
1963
+ * Warn when a body-level `const` that READS a tracked value is then used in a
1964
+ * reactive binding.
1965
+ *
1966
+ * A body-level `const x = <expr>` is evaluated ONCE per render. When `<expr>`
1967
+ * reads a cell, the binding captures a snapshot, so any later re-evaluation of
1968
+ * that binding re-reads the SAME stale value and the UI silently stops
1969
+ * updating — the effect runs, the class/attribute effect runs, and nothing
1970
+ * changes. This has bitten real code twice in one app:
1971
+ *
1972
+ * const isSelected = selectedProgram.id === prog.id; // never re-evaluates
1973
+ * class={`... ${isSelected ? 'a' : 'b'}`} // stuck on 'b'
1974
+ *
1975
+ * The framework cannot fix this silently — hoisting is legitimate when the
1976
+ * value really is constant, and auto-inlining every such const would change
1977
+ * semantics. So it is reported at build time, naming the binding and the
1978
+ * tracked value it snapshotted.
1979
+ *
1980
+ * AST-only (no regex over source), and gated so a component with no such const
1981
+ * costs nothing: the binding scan only runs when a stale candidate exists.
1982
+ */
1983
+ function warnStaleConstBindings(comp, file) {
1984
+ // Pass 1: every tracked/derived name, at ANY depth.
1985
+ const reactive = new Set();
1986
+ const eachNode = (nodes, fn) => {
1987
+ for (const n of nodes) {
1988
+ fn(n);
1989
+ if (n instanceof ComponentCall)
1990
+ eachNode(n.children, fn);
1991
+ if (n instanceof OpaqueDynamicRegion) {
1992
+ eachNode(n.consequentNodes, fn);
1993
+ eachNode(n.alternateNodes, fn);
1994
+ }
1995
+ if (n instanceof MapRegion)
1996
+ eachNode(n.bodyTemplate, fn);
1997
+ if (n instanceof ForLoop)
1998
+ eachNode(n.bodyTemplate, fn);
1999
+ if (n instanceof WhileLoop)
2000
+ eachNode(n.bodyTemplate, fn);
2001
+ if (n instanceof SwitchBlock)
2002
+ for (const c of n.cases)
2003
+ eachNode(c.body, fn);
2004
+ if (n instanceof TryCatch) {
2005
+ eachNode(n.bodyTemplate, fn);
2006
+ eachNode(n.catchBody, fn);
2007
+ }
2008
+ if (n instanceof StaticNode)
2009
+ eachNode(n.children, fn);
2010
+ }
2011
+ };
2012
+ eachNode(comp.body, (n) => {
2013
+ if (!(n instanceof TrackDecl))
2014
+ return;
2015
+ reactive.add(n.name);
2016
+ if (n.rawName)
2017
+ reactive.add(n.rawName);
2018
+ });
2019
+ if (reactive.size === 0)
2020
+ return;
2021
+ // Pass 2: any `const X = <expr>` (at any depth) whose expr reads a tracked value.
2022
+ // Skips aliases of the cell itself (`const &[v, c] = track()` then `const v2 = v`
2023
+ // is still a snapshot, but `const c2 = c` is not) and anything named like a cell.
2024
+ const stale = new Map();
2025
+ eachNode(comp.body, (n) => {
2026
+ if (!(n instanceof RuntimeStatement))
2027
+ return;
2028
+ const st = n.ast;
2029
+ if (st?.type !== 'VariableDeclaration')
2030
+ return;
2031
+ for (const d of st.declarations || []) {
2032
+ if (d.id?.type !== 'Identifier' || !d.init)
2033
+ continue;
2034
+ if (reactive.has(d.id.name))
2035
+ continue;
2036
+ // A bare identifier initializer is an ALIAS, not a read: `const copy =
2037
+ // selCell` re-binds the cell and stays live. Only a read snapshots.
2038
+ if (d.init.type === 'Identifier' && reactive.has(d.init.name))
2039
+ continue;
2040
+ const seen = new Set();
2041
+ collectIdentifiers(d.init, seen);
2042
+ for (const name of seen) {
2043
+ if (!reactive.has(name))
2044
+ continue;
2045
+ stale.set(d.id.name, name);
2046
+ break;
2047
+ }
2048
+ }
2049
+ });
2050
+ if (stale.size === 0)
2051
+ return;
2052
+ // Pass 3: report only when such a name reaches a REACTIVE BINDING. Event
2053
+ // handler attributes are deliberately excluded: `onClick={handler}` where the
2054
+ // handler reads a cell is correct — the read happens when the handler runs,
2055
+ // and the binding is not expected to re-run.
2056
+ const reported = new Set();
2057
+ const report = (name) => {
2058
+ if (reported.has(name))
2059
+ return;
2060
+ const src = stale.get(name);
2061
+ if (!src)
2062
+ return;
2063
+ reported.add(name);
2064
+ console.warn(`[vesk] ${comp.name}: \`const ${name} = ...\` is computed once and reads the tracked value ` +
2065
+ `\`${src}\`, so bindings that use \`${name}\` never update. ` +
2066
+ `Inline the expression in the binding, or wrap the value in derived().`);
2067
+ };
2068
+ const isEventAttr = (target) => !!target && /^on./.test(target);
2069
+ eachNode(comp.body, (n) => {
2070
+ if (n instanceof DynamicBinding) {
2071
+ if (!isEventAttr(n.target)) {
2072
+ const seen = new Set();
2073
+ collectIdentifiers(n.expression.ast, seen);
2074
+ for (const name of seen)
2075
+ if (stale.has(name))
2076
+ report(name);
2077
+ }
2078
+ return;
2079
+ }
2080
+ if (n instanceof ComponentCall) {
2081
+ for (const pr of n.props) {
2082
+ const seen = new Set();
2083
+ collectIdentifiers(pr.value.ast, seen);
2084
+ for (const name of seen)
2085
+ if (stale.has(name))
2086
+ report(name);
2087
+ }
2088
+ }
2089
+ });
2090
+ }
2091
+ /** Every identifier name appearing anywhere in an expression (AST walk). */
2092
+ function collectIdentifiers(node, into) {
2093
+ if (!node || typeof node !== 'object')
2094
+ return;
2095
+ if (Array.isArray(node)) {
2096
+ for (const child of node)
2097
+ collectIdentifiers(child, into);
2098
+ return;
2099
+ }
2100
+ if (typeof node.type !== 'string')
2101
+ return;
2102
+ if (node.type === 'Identifier' && typeof node.name === 'string')
2103
+ into.add(node.name);
2104
+ for (const key of Object.keys(node)) {
2105
+ if (key === 'loc' || key === 'range' || key === 'start' || key === 'end')
2106
+ continue;
2107
+ collectIdentifiers(node[key], into);
2108
+ }
2109
+ }
1962
2110
  export function generateIR(ast, source, filename) {
1963
2111
  __vskAnnotations = ast.__vskAnnotations ?? [];
1964
2112
  __slotProps = null;
@@ -2291,6 +2439,8 @@ export function generateIR(ast, source, filename) {
2291
2439
  imports.push(`import { ${missing.join(', ')} } from '@vesk/runtime';`);
2292
2440
  }
2293
2441
  }
2442
+ for (const comp of components)
2443
+ warnStaleConstBindings(comp, file);
2294
2444
  const irRoot = new IRRoot(components, imports, importedNames, staticProps, loadFn, topLevelCode, exportAliases, reexportSources);
2295
2445
  irRoot.metadataSource = metadataSource;
2296
2446
  return irRoot;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vesk/compiler",
3
- "version": "0.2.55",
3
+ "version": "0.2.56",
4
4
  "type": "module",
5
5
  "description": "Vesk compiler — lexer, parser, semantic analysis, IR, codegen",
6
6
  "main": "./dist/index.js",
@@ -20,7 +20,7 @@
20
20
  },
21
21
  "dependencies": {
22
22
  "@sveltejs/acorn-typescript": "^1.0.0",
23
- "@vesk/types": "^0.2.55",
23
+ "@vesk/types": "^0.2.56",
24
24
  "acorn": "^8.15.0",
25
25
  "esrap": "^2.3.0",
26
26
  "is-reference": "^3.0.0",