@barefootjs/mojolicious 0.5.3 → 0.6.1

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.
@@ -56,7 +56,7 @@ import { isAriaBooleanAttr, isBooleanResultExpr } from './boolean-result'
56
56
  * the `IRNodeEmitter` interface.
57
57
  */
58
58
  type MojoRenderCtx = Record<string, never>
59
- import type { ParsedExpr, ParsedStatement, SortComparator, TemplatePart } from '@barefootjs/jsx'
59
+ import type { ParsedExpr, ParsedStatement, SortComparator, ReduceOp, FlatDepth, FlatMapOp, TemplatePart } from '@barefootjs/jsx'
60
60
  import { BF_SLOT, BF_COND } from '@barefootjs/shared'
61
61
 
62
62
  interface PrimitiveSpec {
@@ -1295,9 +1295,10 @@ function renderArrayMethod(
1295
1295
  // arr.join(sep) → join(sep, @{arr}). The default `${obj}->{join}`
1296
1296
  // hash-lookup fallback would emit invalid Perl, which is why the
1297
1297
  // IR carves out a dedicated method node instead of routing
1298
- // through the generic call dispatcher.
1298
+ // through the generic call dispatcher. `.join()` defaults the
1299
+ // separator to `,` (JS) and ignores any extra argument.
1299
1300
  const obj = emit(object)
1300
- const sep = emit(args[0])
1301
+ const sep = args.length >= 1 ? emit(args[0]) : `','`
1301
1302
  return `join(${sep}, @{${obj}})`
1302
1303
  }
1303
1304
  case 'includes': {
@@ -1333,9 +1334,10 @@ function renderArrayMethod(
1333
1334
  // `.at(i)` with negative-index support — `.at(-1)` is the
1334
1335
  // last element. The Mojo helper wraps the same `length + i`
1335
1336
  // arithmetic the Go `bf_at` does so the lowering stays
1336
- // symmetric across adapters.
1337
+ // symmetric across adapters. `.at()` with no argument is `.at(0)`
1338
+ // (the first element); extra arguments are ignored.
1337
1339
  const obj = emit(object)
1338
- const idx = emit(args[0])
1340
+ const idx = args.length >= 1 ? emit(args[0]) : '0'
1339
1341
  return `bf->at(${obj}, ${idx})`
1340
1342
  }
1341
1343
  case 'concat': {
@@ -1343,20 +1345,26 @@ function renderArrayMethod(
1343
1345
  // ref so the result composes with `.join(...)` / other
1344
1346
  // array-shape methods downstream (the canonical Tier A
1345
1347
  // conformance fixture chains `.concat(...).join(' ')`).
1348
+ // `.concat()` with no argument is a shallow copy — indistinguishable
1349
+ // from the receiver in an SSR snapshot, so it lowers to the receiver.
1350
+ if (args.length === 0) {
1351
+ return emit(object)
1352
+ }
1346
1353
  const a = emit(object)
1347
1354
  const b = emit(args[0])
1348
1355
  return `bf->concat(${a}, ${b})`
1349
1356
  }
1350
1357
  case 'slice': {
1351
- // `.slice(start)` / `.slice(start, end)`. The Mojo helper
1352
- // mirrors the Go arithmetic (negative-index normalisation,
1353
- // out-of-bounds clamping, empty result on start >= end).
1354
- // Absent `end` lowers as `undef`, which the helper treats as
1355
- // "to length". Returns a new ARRAY ref so the result composes
1356
- // with `.join(...)` downstream.
1358
+ // `.slice()` / `.slice(start)` / `.slice(start, end)`. The Mojo
1359
+ // helper mirrors the Go arithmetic (negative-index normalisation,
1360
+ // out-of-bounds clamping, empty result on start >= end). A
1361
+ // missing `start` defaults to 0 (full copy); an absent `end`
1362
+ // lowers as `undef`, which the helper treats as "to length". JS
1363
+ // ignores a third+ argument. Returns a new ARRAY ref so the
1364
+ // result composes with `.join(...)` downstream.
1357
1365
  const recv = emit(object)
1358
- const start = emit(args[0])
1359
- const end = args.length === 2 ? emit(args[1]) : 'undef'
1366
+ const start = args.length >= 1 ? emit(args[0]) : '0'
1367
+ const end = args.length >= 2 ? emit(args[1]) : 'undef'
1360
1368
  return `bf->slice(${recv}, ${start}, ${end})`
1361
1369
  }
1362
1370
  case 'reverse':
@@ -1388,6 +1396,86 @@ function renderArrayMethod(
1388
1396
  const recv = emit(object)
1389
1397
  return `bf->trim(${recv})`
1390
1398
  }
1399
+ case 'split': {
1400
+ // `.split()` / `.split(sep)` / `.split(sep, limit)` — string →
1401
+ // ARRAY ref via `bf->split`. With no separator the helper returns
1402
+ // the whole string as a single element; otherwise it quotemetas
1403
+ // the separator (literal match, not regex) and keeps trailing
1404
+ // empties (`-1`), staying byte-equal with Go's `bf_split`. The
1405
+ // optional `limit` caps the pieces; JS ignores a third+ argument.
1406
+ // See #1448 Tier B.
1407
+ const recv = emit(object)
1408
+ if (args.length === 0) {
1409
+ return `bf->split(${recv})`
1410
+ }
1411
+ const sep = emit(args[0])
1412
+ if (args.length === 1) {
1413
+ return `bf->split(${recv}, ${sep})`
1414
+ }
1415
+ const limit = emit(args[1])
1416
+ return `bf->split(${recv}, ${sep}, ${limit})`
1417
+ }
1418
+ case 'startsWith':
1419
+ case 'endsWith': {
1420
+ // `.startsWith(prefix, position?)` / `.endsWith(suffix,
1421
+ // endPosition?)` — string → boolean. The Perl helpers
1422
+ // (`bf->starts_with` / `bf->ends_with`) do a `substr`-anchored
1423
+ // comparison so the search string is matched literally (no regex
1424
+ // metachar surprises) and undef receivers stay quiet. The optional
1425
+ // second argument re-anchors the test; JS ignores a third+
1426
+ // argument. See #1448 Tier B.
1427
+ const fn = method === 'startsWith' ? 'starts_with' : 'ends_with'
1428
+ const recv = emit(object)
1429
+ const arg = emit(args[0])
1430
+ if (args.length >= 2) {
1431
+ return `bf->${fn}(${recv}, ${arg}, ${emit(args[1])})`
1432
+ }
1433
+ return `bf->${fn}(${recv}, ${arg})`
1434
+ }
1435
+ case 'replace': {
1436
+ // `.replace(old, new)` — string-pattern form, first occurrence.
1437
+ // The `bf->replace` helper splices via index/substr (not `s///`)
1438
+ // so both the pattern and the replacement are literal — no Perl
1439
+ // regex metacharacters and no `$1` / `$&` interpolation in the
1440
+ // replacement, keeping it byte-equal with Go's `bf_replace`. The
1441
+ // regex-pattern form is refused upstream at the parser. See
1442
+ // #1448 Tier B.
1443
+ const recv = emit(object)
1444
+ const oldS = emit(args[0])
1445
+ const newS = emit(args[1])
1446
+ return `bf->replace(${recv}, ${oldS}, ${newS})`
1447
+ }
1448
+ case 'repeat': {
1449
+ // `.repeat(n)` — string repeated `n` times. The `bf->repeat`
1450
+ // helper wraps Perl's `x` operator with the same negative-count
1451
+ // → "" clamp and integer truncation Go's `bf_repeat` applies, so
1452
+ // the two adapters stay byte-equal. Full JS arity: the no-argument
1453
+ // form is `repeat(0)` → ""; a second+ argument is ignored.
1454
+ // See #1448 Tier B.
1455
+ const recv = emit(object)
1456
+ const count = args.length === 0 ? '0' : emit(args[0])
1457
+ return `bf->repeat(${recv}, ${count})`
1458
+ }
1459
+ case 'padStart':
1460
+ case 'padEnd': {
1461
+ // `.padStart(target, pad?)` / `.padEnd(target, pad?)`. The
1462
+ // `bf->pad_*` helpers default the pad to a single space when the
1463
+ // arg is omitted and measure length in characters, matching Go's
1464
+ // rune-based `bf_pad_*`. Full JS arity: the no-argument form is
1465
+ // `padStart(0)` → the receiver unchanged; a third+ argument is
1466
+ // ignored. See #1448 Tier B.
1467
+ const fn = method === 'padStart' ? 'pad_start' : 'pad_end'
1468
+ const recv = emit(object)
1469
+ if (args.length === 0) {
1470
+ return `bf->${fn}(${recv}, 0)`
1471
+ }
1472
+ const target = emit(args[0])
1473
+ if (args.length === 1) {
1474
+ return `bf->${fn}(${recv}, ${target})`
1475
+ }
1476
+ const pad = emit(args[1])
1477
+ return `bf->${fn}(${recv}, ${target}, ${pad})`
1478
+ }
1391
1479
  default: {
1392
1480
  // TS-level exhaustiveness guard. If this throws at runtime, the
1393
1481
  // IR was constructed against a newer `ArrayMethod` variant that
@@ -1445,6 +1533,64 @@ function renderSortMethod(recv: string, c: SortComparator): string {
1445
1533
  return `bf->sort(${recv}, { keys => [${keyHashes.join(', ')}] })`
1446
1534
  }
1447
1535
 
1536
+ /**
1537
+ * Render a `.reduce(fn, init)` arithmetic fold (#1448 Tier C) as a
1538
+ * `bf->reduce(...)` call. The structured `ReduceOp` maps to the Perl
1539
+ * helper's options hash:
1540
+ *
1541
+ * bf->reduce($recv, { op => '+', key_kind => 'field', key => 'duration',
1542
+ * type => 'numeric', init => 0 })
1543
+ *
1544
+ * A numeric init passes through as a bare Perl number (`0`, `-1`); a
1545
+ * string init (concat fold) is re-quoted from its literal contents.
1546
+ */
1547
+ function renderReduceMethod(recv: string, op: ReduceOp, direction: 'left' | 'right'): string {
1548
+ const keyEntry =
1549
+ op.key.kind === 'self'
1550
+ ? `key_kind => 'self'`
1551
+ : `key_kind => 'field', key => '${op.key.field}'`
1552
+ // `op.init` is the decoded seed value. A numeric seed is already a
1553
+ // canonical decimal literal Perl reads directly; a concat seed is the
1554
+ // string contents, embedded in a single-quoted Perl literal. The `'`
1555
+ // escape is REQUIRED: a seed decoded from a double-quoted JS literal
1556
+ // (e.g. `"a'b"`) is escape-free yet contains an apostrophe. A literal
1557
+ // backslash can't occur (it would need a `\\` escape, which the parser
1558
+ // refuses), but escaping it too keeps this self-contained.
1559
+ const init =
1560
+ op.type === 'string'
1561
+ ? `'${op.init.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`
1562
+ : op.init
1563
+ // `direction` is "left" (reduce) or "right" (reduceRight); the Perl
1564
+ // helper reverses the list for "right". Only observable for concat.
1565
+ return `bf->reduce(${recv}, { op => '${op.op}', ${keyEntry}, type => '${op.type}', init => ${init}, direction => '${direction}' })`
1566
+ }
1567
+
1568
+ // `.flat(depth?)` → `bf->flat($recv, $depth)`. The `Infinity` form lowers
1569
+ // to the `-1` sentinel (flatten fully); a finite depth flattens that many
1570
+ // levels (`0` = shallow copy). See `sub flat` in BarefootJS.pm. (#1448)
1571
+ function renderFlatMethod(recv: string, depth: FlatDepth): string {
1572
+ const d = depth === 'infinity' ? -1 : depth
1573
+ return `bf->flat(${recv}, ${d})`
1574
+ }
1575
+
1576
+ // `.flatMap(i => i)` / `.flatMap(i => i.field)` → `bf->flat_map($recv,
1577
+ // 'self'|'field', 'field')`, and the array-literal tuple form
1578
+ // `i => [i.a, i.b]` → `bf->flat_map_tuple($recv, ['field','a'], ...)`
1579
+ // (one arrayref per leaf). The field key is the raw JS prop name (Perl
1580
+ // hashes are keyed by it), mirroring `bf->reduce`. See `sub flat_map` /
1581
+ // `sub flat_map_tuple` in BarefootJS.pm.
1582
+ function renderFlatMapMethod(recv: string, op: FlatMapOp): string {
1583
+ const proj = op.projection
1584
+ if (proj.kind === 'tuple') {
1585
+ const specs = proj.elements
1586
+ .map(l => (l.kind === 'self' ? `['self', '']` : `['field', '${l.field}']`))
1587
+ .join(', ')
1588
+ return `bf->flat_map_tuple(${recv}, ${specs})`
1589
+ }
1590
+ if (proj.kind === 'self') return `bf->flat_map(${recv}, 'self', '')`
1591
+ return `bf->flat_map(${recv}, 'field', '${proj.field}')`
1592
+ }
1593
+
1448
1594
  /** True when `type` is the `string` primitive. */
1449
1595
  function isStringTypeInfo(type: TypeInfo | undefined): boolean {
1450
1596
  return type?.kind === 'primitive' && type.primitive === 'string'
@@ -1627,6 +1773,18 @@ class MojoFilterEmitter implements ParsedExprEmitter {
1627
1773
  return renderSortMethod(emit(object), comparator)
1628
1774
  }
1629
1775
 
1776
+ reduceMethod(method: 'reduce' | 'reduceRight', object: ParsedExpr, reduceOp: ReduceOp, emit: (e: ParsedExpr) => string): string {
1777
+ return renderReduceMethod(emit(object), reduceOp, method === 'reduceRight' ? 'right' : 'left')
1778
+ }
1779
+
1780
+ flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
1781
+ return renderFlatMethod(emit(object), depth)
1782
+ }
1783
+
1784
+ flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
1785
+ return renderFlatMapMethod(emit(object), op)
1786
+ }
1787
+
1630
1788
  conditional(_test: ParsedExpr, _consequent: ParsedExpr, _alternate: ParsedExpr): string {
1631
1789
  return '1'
1632
1790
  }
@@ -1808,6 +1966,18 @@ class MojoTopLevelEmitter implements ParsedExprEmitter {
1808
1966
  return renderSortMethod(emit(object), comparator)
1809
1967
  }
1810
1968
 
1969
+ reduceMethod(method: 'reduce' | 'reduceRight', object: ParsedExpr, reduceOp: ReduceOp, emit: (e: ParsedExpr) => string): string {
1970
+ return renderReduceMethod(emit(object), reduceOp, method === 'reduceRight' ? 'right' : 'left')
1971
+ }
1972
+
1973
+ flatMethod(object: ParsedExpr, depth: FlatDepth, emit: (e: ParsedExpr) => string): string {
1974
+ return renderFlatMethod(emit(object), depth)
1975
+ }
1976
+
1977
+ flatMapMethod(object: ParsedExpr, op: FlatMapOp, emit: (e: ParsedExpr) => string): string {
1978
+ return renderFlatMapMethod(emit(object), op)
1979
+ }
1980
+
1811
1981
  conditional(
1812
1982
  test: ParsedExpr,
1813
1983
  consequent: ParsedExpr,