@pyreon/compiler 0.14.0 → 0.16.0

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.
package/src/jsx.ts CHANGED
@@ -29,30 +29,21 @@
29
29
  */
30
30
 
31
31
  import { parseSync } from 'oxc-parser'
32
- import { createRequire } from 'node:module'
33
- import { fileURLToPath } from 'node:url'
34
- import { dirname, join } from 'node:path'
32
+ import { REACT_EVENT_REMAP } from './event-names'
33
+ import { loadNativeBinding } from './load-native'
35
34
 
36
35
  // ─── Native binary auto-detection ────────────────────────────────────────────
37
- // Try to load the Rust napi-rs binary for 3.7-8.2x faster transforms.
38
- // Falls back to the JS implementation below if the binary isn't available
39
- // (wrong platform, CI environment, WASM runtime like StackBlitz, etc.)
36
+ // Two-path resolution: in-tree binary first (dev mode), then per-platform
37
+ // npm package (production install via optionalDependencies). Falls through
38
+ // to the JS implementation below when both paths fail (wrong platform, CI
39
+ // environment, WASM runtime like StackBlitz, missing per-platform package).
40
40
  //
41
- // Uses createRequire for ESM compatibility — __dirname and require() don't
42
- // exist in ESM modules.
41
+ // See `load-native.ts` for the resolution logic.
43
42
  type NativeTransformFn = (code: string, filename: string, ssr: boolean, knownSignals: string[] | null) => TransformResult
44
- let nativeTransformJsx: NativeTransformFn | null = null
45
-
46
- try {
47
- const __filename = fileURLToPath(import.meta.url)
48
- const __dirname = dirname(__filename)
49
- const nativeRequire = createRequire(import.meta.url)
50
- const nativePath = join(__dirname, '..', 'native', 'pyreon-compiler.node')
51
- const native = nativeRequire(nativePath) as { transformJsx: NativeTransformFn }
52
- nativeTransformJsx = native.transformJsx
53
- } catch {
54
- // Native binary not available — JS fallback will be used
55
- }
43
+ const nativeBinding = loadNativeBinding(import.meta.url)
44
+ const nativeTransformJsx: NativeTransformFn | null = nativeBinding
45
+ ? (nativeBinding.transformJsx as NativeTransformFn)
46
+ : null
56
47
 
57
48
  export interface CompilerWarning {
58
49
  /** Warning message */
@@ -773,19 +764,19 @@ export function transformJSX_JS(
773
764
  if (replacements.length === 0 && hoists.length === 0) return { code, warnings }
774
765
 
775
766
  replacements.sort((a, b) => a.start - b.start)
776
- const parts: string[] = []
777
- let lastPos = 0
767
+ const outParts: string[] = []
768
+ let outPos = 0
778
769
  for (const r of replacements) {
779
- parts.push(code.slice(lastPos, r.start))
780
- parts.push(r.text)
781
- lastPos = r.end
770
+ outParts.push(code.slice(outPos, r.start))
771
+ outParts.push(r.text)
772
+ outPos = r.end
782
773
  }
783
- parts.push(code.slice(lastPos))
784
- let result = parts.join('')
774
+ outParts.push(code.slice(outPos))
775
+ let output = outParts.join('')
785
776
 
786
777
  if (hoists.length > 0) {
787
778
  const preamble = hoists.map((h) => `const ${h.name} = /*@__PURE__*/ ${h.text}\n`).join('')
788
- result = preamble + result
779
+ output = preamble + output
789
780
  }
790
781
 
791
782
  if (needsTplImport) {
@@ -797,16 +788,16 @@ export function transformJSX_JS(
797
788
  const reactivityImports = needsBindImportGlobal
798
789
  ? `\nimport { _bind } from "@pyreon/reactivity";`
799
790
  : ''
800
- result =
791
+ output =
801
792
  `import { ${runtimeDomImports.join(', ')} } from "@pyreon/runtime-dom";${reactivityImports}\n` +
802
- result
793
+ output
803
794
  }
804
795
 
805
796
  if (needsRpImport) {
806
- result = `import { _rp } from "@pyreon/core";\n` + result
797
+ output = `import { _rp } from "@pyreon/core";\n` + output
807
798
  }
808
799
 
809
- return { code: result, usesTemplates: needsTplImport, warnings }
800
+ return { code: output, usesTemplates: needsTplImport, warnings }
810
801
 
811
802
  // ── Template emission helpers ─────────────────────────────────────────────
812
803
 
@@ -901,7 +892,26 @@ export function transformJSX_JS(
901
892
  }
902
893
 
903
894
  function emitEventListener(attr: N, attrName: string, varName: string): void {
904
- const eventName = (attrName[2] ?? '').toLowerCase() + attrName.slice(3)
895
+ // Translate the JSX-style React attribute name (e.g. `onKeyDown`,
896
+ // `onDoubleClick`) to the canonical DOM event name (`keydown`,
897
+ // `dblclick`).
898
+ //
899
+ // The default rule is "drop the `on` prefix and lowercase" —
900
+ // covers `onKeyDown` → `keydown`, `onMouseEnter` → `mouseenter`,
901
+ // `onPointerLeave` → `pointerleave`, `onAnimationStart` →
902
+ // `animationstart`, etc. Most React event names follow this rule
903
+ // because the underlying DOM event name is also the lowercased
904
+ // multi-word form.
905
+ //
906
+ // The exception list lives in `REACT_EVENT_REMAP` (event-names.ts).
907
+ // Every React event-prop in the official component-prop list was
908
+ // audited against canonical DOM event names — see the JSDoc on
909
+ // REACT_EVENT_REMAP for the audit. Today exactly one entry:
910
+ // `onDoubleClick` → `dblclick`
911
+ // The Rust native backend (`native/src/lib.rs:emit_event_listener`)
912
+ // mirrors the same table — keep them in sync if a new entry is added.
913
+ const lowered = attrName.slice(2).toLowerCase()
914
+ const eventName = REACT_EVENT_REMAP[lowered] ?? lowered
905
915
  if (!attr.value || attr.value.type !== 'JSXExpressionContainer') return
906
916
  const expr = attr.value.expression
907
917
  if (!expr || expr.type === 'JSXEmptyExpression') return
@@ -952,6 +962,7 @@ export function transformJSX_JS(
952
962
  function attrSetter(htmlAttrName: string, varName: string, expr: string): string {
953
963
  if (htmlAttrName === 'class') return `${varName}.className = ${expr}`
954
964
  if (htmlAttrName === 'style') return `${varName}.style.cssText = ${expr}`
965
+ if (DOM_PROPS.has(htmlAttrName)) return `${varName}.${htmlAttrName} = ${expr}`
955
966
  return `${varName}.setAttribute("${htmlAttrName}", ${expr})`
956
967
  }
957
968
 
@@ -970,7 +981,9 @@ export function transformJSX_JS(
970
981
  ? `(v) => { ${varName}.className = v == null ? "" : String(v) }`
971
982
  : htmlAttrName === 'style'
972
983
  ? `(v) => { if (typeof v === "string") ${varName}.style.cssText = v; else if (v) Object.assign(${varName}.style, v) }`
973
- : `(v) => { ${varName}.setAttribute("${htmlAttrName}", v == null ? "" : String(v)) }`
984
+ : DOM_PROPS.has(htmlAttrName)
985
+ ? `(v) => { ${varName}.${htmlAttrName} = v }`
986
+ : `(v) => { ${varName}.setAttribute("${htmlAttrName}", v == null ? "" : String(v)) }`
974
987
  bindLines.push(`const ${d} = _bindDirect(${directRef}, ${updater})`)
975
988
  return
976
989
  }
@@ -1080,8 +1093,8 @@ export function transformJSX_JS(
1080
1093
  ): void {
1081
1094
  if (child.type === 'JSXText') {
1082
1095
  const raw = child.value ?? child.raw ?? ''
1083
- const trimmed = raw.replace(/\n\s*/g, '').trim()
1084
- if (trimmed) out.push({ kind: 'text', text: trimmed })
1096
+ const cleaned = cleanJsxText(raw)
1097
+ if (cleaned) out.push({ kind: 'text', text: cleaned })
1085
1098
  return
1086
1099
  }
1087
1100
  if (child.type === 'JSXElement') {
@@ -1108,9 +1121,19 @@ export function transformJSX_JS(
1108
1121
 
1109
1122
  function analyzeChildren(flatChildren: FlatChild[]): { useMixed: boolean; useMultiExpr: boolean } {
1110
1123
  const hasElem = flatChildren.some((c) => c.kind === 'element')
1111
- const hasNonElem = flatChildren.some((c) => c.kind !== 'element')
1124
+ const hasText = flatChildren.some((c) => c.kind === 'text')
1112
1125
  const exprCount = flatChildren.filter((c) => c.kind === 'expression').length
1113
- return { useMixed: hasElem && hasNonElem, useMultiExpr: exprCount > 1 }
1126
+ // `useMixed` triggers placeholder-based positional mounting (each
1127
+ // dynamic child gets a `<!>` comment slot in the template that
1128
+ // `replaceChild`-replaces at mount). It must fire whenever ≥2 of
1129
+ // {element, text, expression} are interleaved — otherwise dynamic
1130
+ // text nodes added via `appendChild` land after all static
1131
+ // template content, breaking source-order rendering for shapes
1132
+ // like `<p>foo {x()} bar</p>` (rendered "foo barX" instead of
1133
+ // "foo X bar"). Discovered by Phase B2's whitespace tests.
1134
+ const present =
1135
+ (hasElem ? 1 : 0) + (hasText ? 1 : 0) + (exprCount > 0 ? 1 : 0)
1136
+ return { useMixed: present > 1, useMultiExpr: exprCount > 1 }
1114
1137
  }
1115
1138
 
1116
1139
  function attrIsDynamic(attr: N): boolean {
@@ -1210,7 +1233,31 @@ export function transformJSX_JS(
1210
1233
  return `_tpl("${escaped}", () => null)`
1211
1234
  }
1212
1235
 
1213
- let body = bindLines.map((l) => ` ${l}`).join('\n')
1236
+ // Append `;` to every bind line so ASI can't merge consecutive
1237
+ // statements when the next line starts with `(`, `[`, etc.
1238
+ // Concrete bug shape (pre-fix): a child element with `hasDynamic=true`
1239
+ // emits `const __e0 = __root.children[N]` followed by a ref-callback
1240
+ // line `((el) => { x = el })(__e0)`. JS does NOT insert ASI here
1241
+ // because `__root.children[N]((el) => ...)` is a valid expression,
1242
+ // so the parser merges them into a single function call:
1243
+ // `const __e0 = __root.children[N]((el) => ...)(__e0)`
1244
+ // — calling `children[N]` as a function with the arrow as argument,
1245
+ // and self-referencing `__e0` before assignment. Adding the `;`
1246
+ // terminates each statement deterministically. Trailing `;` after
1247
+ // a `{...}` block is a harmless empty statement.
1248
+ // Append `;` to every bind line so ASI can't merge consecutive
1249
+ // statements when the next line starts with `(`, `[`, etc.
1250
+ // Concrete bug shape (pre-fix): a child element with `hasDynamic=true`
1251
+ // emits `const __e0 = __root.children[N]` followed by a ref-callback
1252
+ // line `((el) => { x = el })(__e0)`. JS does NOT insert ASI here
1253
+ // because `__root.children[N]((el) => ...)` is a valid expression,
1254
+ // so the parser merges them into a single function call:
1255
+ // `const __e0 = __root.children[N]((el) => ...)(__e0)`
1256
+ // — calling `children[N]` as a function with the arrow as argument,
1257
+ // and self-referencing `__e0` before assignment. Adding the `;`
1258
+ // terminates each statement deterministically. Trailing `;` after
1259
+ // a `{...}` block is a harmless empty statement.
1260
+ let body = bindLines.map((l) => ` ${l};`).join('\n')
1214
1261
  if (disposerNames.length > 0) {
1215
1262
  body += `\n return () => { ${disposerNames.map((d) => `${d}()`).join('; ')} }`
1216
1263
  } else {
@@ -1244,6 +1291,16 @@ export function transformJSX_JS(
1244
1291
  if (node.type === 'Identifier' && isActiveSignal(node.name)) {
1245
1292
  const parent = findParent(node)
1246
1293
  if (parent && parent.type === 'MemberExpression' && parent.property === node && !parent.computed) return false
1294
+ // signal.X(...) — operating on the signal object (calling a method).
1295
+ // Mirrors the same narrow skip in findSignalIdents below.
1296
+ if (
1297
+ parent &&
1298
+ parent.type === 'MemberExpression' &&
1299
+ parent.object === node
1300
+ ) {
1301
+ const grand = findParent(parent)
1302
+ if (grand && grand.type === 'CallExpression' && grand.callee === parent) return false
1303
+ }
1247
1304
  if (parent && parent.type === 'CallExpression' && parent.callee === node) return false // already called
1248
1305
  return true
1249
1306
  }
@@ -1269,6 +1326,27 @@ export function transformJSX_JS(
1269
1326
  const parent = findParent(node)
1270
1327
  // Skip property name positions (obj.name)
1271
1328
  if (parent && parent.type === 'MemberExpression' && parent.property === node && !parent.computed) return
1329
+ // Skip when the identifier is the OBJECT of a member access AND
1330
+ // the result is being CALLED (signal.set(...), signal.peek(),
1331
+ // signal.update(...)). The user is invoking a method on the
1332
+ // signal OBJECT — auto-calling would produce `signal().set(...)`
1333
+ // which calls the signal, gets its value (string/number/etc),
1334
+ // then `.set` on the value is undefined → TypeError. Every event
1335
+ // handler that did `signal.set(x)` was silently broken.
1336
+ //
1337
+ // Note: bare `signal.value` (member access NOT followed by call)
1338
+ // STILL auto-calls — keeps the existing convention where
1339
+ // `signal({a:1})` followed by `signal.a` reads the signal's
1340
+ // value's property (see "signal as member expression object IS
1341
+ // auto-called" test).
1342
+ if (
1343
+ parent &&
1344
+ parent.type === 'MemberExpression' &&
1345
+ parent.object === node
1346
+ ) {
1347
+ const grand = findParent(parent)
1348
+ if (grand && grand.type === 'CallExpression' && grand.callee === parent) return
1349
+ }
1272
1350
  // Skip if already being called: signal()
1273
1351
  if (parent && parent.type === 'CallExpression' && parent.callee === node) return
1274
1352
  // Skip declaration positions
@@ -1313,6 +1391,24 @@ const JSX_TO_HTML_ATTR: Record<string, string> = {
1313
1391
  htmlFor: 'for',
1314
1392
  }
1315
1393
 
1394
+ // DOM properties whose live value diverges from the content attribute.
1395
+ // For these, emit property assignment (`el.value = v`) instead of
1396
+ // `setAttribute("value", v)`. Otherwise the property and attribute drift
1397
+ // apart in user-driven flows: typing in a controlled <input> updates the
1398
+ // .value property, but `input.set('')` clearing the signal only resets
1399
+ // the attribute — the stale typed text stays visible. Same for `checked`
1400
+ // on checkboxes (presence of the attribute means checked regardless of
1401
+ // value: `setAttribute("checked", "false")` still checks the box).
1402
+ const DOM_PROPS = new Set([
1403
+ 'value',
1404
+ 'checked',
1405
+ 'selected',
1406
+ 'disabled',
1407
+ 'multiple',
1408
+ 'readOnly',
1409
+ 'indeterminate',
1410
+ ])
1411
+
1316
1412
  const STATEFUL_CALLS = new Set([
1317
1413
  'signal', 'computed', 'effect', 'batch',
1318
1414
  'createContext', 'createReactiveContext',
@@ -1364,6 +1460,33 @@ function escapeHtmlText(s: string): string {
1364
1460
  return s.replace(/&(?!(?:#\d+|#x[\da-fA-F]+|[a-zA-Z]\w*);)/g, '&amp;').replace(/</g, '&lt;')
1365
1461
  }
1366
1462
 
1463
+ // React/Babel JSX whitespace algorithm (cleanJSXElementLiteralChild).
1464
+ // Same-line text is preserved verbatim so adjacent expressions keep their
1465
+ // spacing (`<p>doubled: {x}</p>` keeps the trailing space). Multi-line text
1466
+ // strips leading whitespace from non-first lines and trailing whitespace
1467
+ // from non-last lines, drops fully-empty lines, and joins the survivors
1468
+ // with a single space — collapsing JSX indentation without losing
1469
+ // intentional inline spacing.
1470
+ function cleanJsxText(raw: string): string {
1471
+ if (!raw.includes('\n') && !raw.includes('\r')) return raw
1472
+ const lines = raw.split(/\r\n|\n|\r/)
1473
+ let lastNonEmpty = -1
1474
+ for (let i = 0; i < lines.length; i++) {
1475
+ if (/[^ \t]/.test(lines[i] ?? '')) lastNonEmpty = i
1476
+ }
1477
+ let str = ''
1478
+ for (let i = 0; i < lines.length; i++) {
1479
+ let line = (lines[i] ?? '').replace(/\t/g, ' ')
1480
+ if (i !== 0) line = line.replace(/^ +/, '')
1481
+ if (i !== lines.length - 1) line = line.replace(/ +$/, '')
1482
+ if (line) {
1483
+ if (i !== lastNonEmpty) line += ' '
1484
+ str += line
1485
+ }
1486
+ }
1487
+ return str
1488
+ }
1489
+
1367
1490
  function isStaticJSXNode(node: N): boolean {
1368
1491
  if (node.type === 'JSXElement' && node.openingElement?.selfClosing) {
1369
1492
  return isStaticAttrs(node.openingElement.attributes ?? [])
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Native binding loader — resolves the @pyreon/compiler napi-rs binary
3
+ * via two paths in priority order:
4
+ *
5
+ * 1. **In-tree binary** at `<package>/native/pyreon-compiler.node`.
6
+ * Populated by `scripts/build-native.ts` during local development
7
+ * (Phase 2). Faster path because it skips npm-package resolution.
8
+ *
9
+ * 2. **Per-platform npm package** (Phase 5b — not active until per-
10
+ * platform packages are published). Resolves `@pyreon/compiler-
11
+ * <platform>-<arch>[-<libc>]` via the standard Node module
12
+ * resolution algorithm. End users on machines without a local
13
+ * `cargo` install will hit this path: `bun install` resolves
14
+ * `optionalDependencies` to the matching per-platform package and
15
+ * this loader picks it up.
16
+ *
17
+ * 3. **JS fallback** (caller's responsibility) — if both paths fail,
18
+ * `loadNativeBinding()` returns `null` and the caller uses the
19
+ * pure-JS implementation. Slower but correctness-equivalent.
20
+ *
21
+ * Platform detection follows the napi-rs convention. Linux variants
22
+ * include a `libc` suffix (`gnu` for glibc, `musl` for musl) per
23
+ * https://napi.rs/docs/cli/build#deployment.
24
+ *
25
+ * The two-path resolution lets dev-mode (where `cargo build` produced
26
+ * an in-tree binary) and production-mode (where the user has only the
27
+ * published per-platform package) coexist with no flag flipping.
28
+ */
29
+
30
+ import { createRequire } from 'node:module'
31
+ import { fileURLToPath } from 'node:url'
32
+ import { dirname, join } from 'node:path'
33
+
34
+ export interface NativeBinding {
35
+ transformJsx: (
36
+ code: string,
37
+ filename: string,
38
+ ssr: boolean,
39
+ knownSignals: string[] | null,
40
+ ) => unknown
41
+ }
42
+
43
+ // Local Node-process surface. `@pyreon/runtime-dom` ships an ambient
44
+ // `declare var process: { env: { NODE_ENV?: string } }` to enforce the
45
+ // bundler-agnostic dev-gate pattern, which narrows `process` for ANY
46
+ // file pulled in by runtime-dom's typecheck — including this one when
47
+ // imported via the `bun` condition. Casting through a local interface
48
+ // restores access to the platform/arch/report fields we genuinely need.
49
+ interface NodeProcess {
50
+ platform: string
51
+ arch: string
52
+ report?: {
53
+ getReport(): unknown
54
+ }
55
+ }
56
+ const nodeProcess = process as unknown as NodeProcess
57
+
58
+ /**
59
+ * Resolve the per-platform package name following the napi-rs naming
60
+ * convention: `@pyreon/compiler-<platform>-<arch>[-<libc>]`.
61
+ *
62
+ * Examples:
63
+ * darwin + arm64 → @pyreon/compiler-darwin-arm64
64
+ * darwin + x64 → @pyreon/compiler-darwin-x64
65
+ * linux + x64 + gnu → @pyreon/compiler-linux-x64-gnu
66
+ * linux + arm64 + gnu → @pyreon/compiler-linux-arm64-gnu
67
+ * win32 + x64 + msvc → @pyreon/compiler-win32-x64-msvc
68
+ *
69
+ * Returns `null` for unsupported (platform, arch) combinations — caller
70
+ * skips per-platform resolution entirely and falls through to JS.
71
+ */
72
+ export function getPlatformPackageName(
73
+ platform: string = nodeProcess.platform,
74
+ arch: string = nodeProcess.arch,
75
+ libc: string | null = detectLibc(platform),
76
+ ): string | null {
77
+ // Build the suffix for libc-bearing platforms (Linux glibc/musl,
78
+ // Windows MSVC). Single source of truth — no per-platform branching.
79
+ const suffix = libc ? `-${libc}` : ''
80
+ // Allowlist of (platform, arch) combos that the cross-platform CI
81
+ // workflow actually builds. Keep in sync with
82
+ // `.github/workflows/release-native.yml` matrix.
83
+ const supported: Record<string, string[]> = {
84
+ darwin: ['arm64', 'x64'],
85
+ linux: ['x64', 'arm64'],
86
+ win32: ['x64'],
87
+ }
88
+ if (!supported[platform]?.includes(arch)) return null
89
+ return `@pyreon/compiler-${platform}-${arch}${suffix}`
90
+ }
91
+
92
+ /**
93
+ * Detect the libc family for the current Linux runtime. Returns:
94
+ * - `'gnu'` on glibc-based distros (Debian, Ubuntu, RHEL, …)
95
+ * - `'musl'` on musl-based distros (Alpine, …)
96
+ * - `null` on macOS / Windows (no libc differentiation)
97
+ * - `'msvc'` on Windows (we only ship MSVC binaries)
98
+ *
99
+ * `process.report.getReport().header.glibcVersionRuntime` is the
100
+ * Node-canonical detection: present on glibc, absent on musl. Falls
101
+ * back to `gnu` on read failure since glibc is the more common case.
102
+ */
103
+ function detectLibc(platform: string): string | null {
104
+ if (platform === 'win32') return 'msvc'
105
+ if (platform !== 'linux') return null
106
+ try {
107
+ const report = nodeProcess.report?.getReport()
108
+ if (typeof report === 'object' && report !== null) {
109
+ const header = (report as { header?: { glibcVersionRuntime?: string } }).header
110
+ return header?.glibcVersionRuntime ? 'gnu' : 'musl'
111
+ }
112
+ } catch {
113
+ // Best-effort detection — fall through to glibc default.
114
+ }
115
+ return 'gnu'
116
+ }
117
+
118
+ /**
119
+ * Load the native binding by trying paths in order:
120
+ * 1. In-tree binary (`<package>/native/pyreon-compiler.node`)
121
+ * 2. Per-platform npm package (`@pyreon/compiler-<triple>`)
122
+ *
123
+ * Returns `null` if both paths fail — caller falls back to the
124
+ * pure-JS implementation. NEVER throws — every error path swallows
125
+ * silently because a missing native binary is a perf optimization
126
+ * miss, not a correctness failure.
127
+ */
128
+ export function loadNativeBinding(metaUrl: string): NativeBinding | null {
129
+ const nativeRequire = createRequire(metaUrl)
130
+
131
+ // Path 1: in-tree binary (dev mode + Phase 2 local-build path).
132
+ try {
133
+ const __filename = fileURLToPath(metaUrl)
134
+ const __dirname = dirname(__filename)
135
+ const nativePath = join(__dirname, '..', 'native', 'pyreon-compiler.node')
136
+ return nativeRequire(nativePath) as NativeBinding
137
+ } catch {
138
+ // In-tree binary not present — fall through to per-platform package.
139
+ }
140
+
141
+ // Path 2: per-platform npm package (production install path).
142
+ // Will start working once Phase 5b publishes the per-platform
143
+ // packages and `optionalDependencies` resolves them at install time.
144
+ const pkgName = getPlatformPackageName()
145
+ if (pkgName !== null) {
146
+ try {
147
+ return nativeRequire(pkgName) as NativeBinding
148
+ } catch {
149
+ // Per-platform package not installed (typical pre-Phase-5b
150
+ // state, or a platform we don't yet ship binaries for).
151
+ }
152
+ }
153
+
154
+ return null
155
+ }