@barefootjs/go-template 0.1.2 → 0.1.3

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.
@@ -71,6 +71,7 @@ type GoRenderCtx = {
71
71
  */
72
72
  interface NestedComponentInfo extends IRLoopChildComponent {
73
73
  isDynamic: boolean
74
+ isPropDerived: boolean
74
75
  }
75
76
 
76
77
  interface StaticChildInstance {
@@ -308,6 +309,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
308
309
  * follow-up).
309
310
  */
310
311
  private restPropsName: string | null = null
312
+ private templateVarCounter: number = 0
311
313
  /** Local type names resolved from typeDefinitions (populated during generateTypes) */
312
314
  private localTypeNames: Set<string> = new Set()
313
315
  /** Local type aliases mapping type name to base type (e.g., Filter → 'string') */
@@ -334,6 +336,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
334
336
  generate(ir: ComponentIR, options?: AdapterGenerateOptions): AdapterOutput {
335
337
  this.componentName = ir.metadata.componentName
336
338
  this.errors = []
339
+ this.templateVarCounter = 0
337
340
  this.propsObjectName = ir.metadata.propsObjectName
338
341
  this.restPropsName = ir.metadata.restPropsName ?? null
339
342
 
@@ -807,8 +810,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
807
810
  lines.push('\tBfParent string // Optional: parent scope id')
808
811
  lines.push('\tBfMount string // Optional: slot id in parent')
809
812
 
810
- // Static nested components appear in Input; dynamic ones are template-only
811
- const staticNested = nestedComponents.filter(n => !n.isDynamic)
813
+ // Static + prop-derived nested components appear in Input;
814
+ // signal-backed dynamic ones are template-only
815
+ const inputNested = nestedComponents.filter(n => !n.isDynamic || n.isPropDerived)
812
816
 
813
817
  // Collect nested component array field names to skip from propsParams
814
818
  const nestedArrayFields = new Set(nestedComponents.map(n => `${n.name}s`))
@@ -821,8 +825,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
821
825
  lines.push(`\t${fieldName} ${goType}`)
822
826
  }
823
827
 
824
- // Add nested component input arrays (static only)
825
- for (const nested of staticNested) {
828
+ // Add nested component input arrays
829
+ for (const nested of inputNested) {
826
830
  lines.push(`\t${nested.name}s []${nested.name}Input`)
827
831
  }
828
832
 
@@ -953,10 +957,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
953
957
 
954
958
  // Add array fields for nested components (for template rendering)
955
959
  for (const nested of nestedComponents) {
956
- if (nested.isDynamic) {
957
- // Dynamic (signal) array loops: template-only, not in JSON
960
+ if (nested.isDynamic && !nested.isPropDerived) {
961
+ // Dynamic signal array loops: template-only, not in JSON
958
962
  lines.push(`\t${nested.name}s []${nested.name}Props \`json:"-"\``)
959
963
  } else {
964
+ // Static arrays and prop-derived dynamic arrays: include in JSON
965
+ // so the client can hydrate via mapArray or forEach
960
966
  const jsonTag = this.toJsonTag(`${nested.name.charAt(0).toLowerCase()}${nested.name.slice(1)}s`)
961
967
  lines.push(`\t${nested.name}s []${nested.name}Props \`json:"${jsonTag}"\``)
962
968
  }
@@ -1005,9 +1011,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1005
1011
  // field, the SSR template iterates over it, but
1006
1012
  // `NewTodoAppProps(TodoAppInput{Initial: ...})` returns it empty
1007
1013
  // and the page renders a blank list (#1442 echo TodoApp repro).
1008
- const dynamicNested = nestedComponents.filter(n => n.isDynamic)
1014
+ const signalDynamicNested = nestedComponents.filter(n => n.isDynamic && !n.isPropDerived)
1009
1015
  lines.push(`// New${componentName}Props creates ${propsTypeName} from ${inputTypeName}.`)
1010
- for (const nested of dynamicNested) {
1016
+ for (const nested of signalDynamicNested) {
1011
1017
  const arrayField = `${nested.name}s`
1012
1018
  lines.push(`//`)
1013
1019
  lines.push(`// NOTE: \`${arrayField}\` is populated by the route handler, not by`)
@@ -1030,8 +1036,9 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1030
1036
  lines.push('\t}')
1031
1037
  lines.push('')
1032
1038
 
1033
- // Static nested components only dynamic ones are set manually by the handler
1034
- const staticNested = nestedComponents.filter(n => !n.isDynamic)
1039
+ // Static + prop-derived nested components: auto-populate from input.
1040
+ // Signal-backed dynamic arrays are set manually by the handler.
1041
+ const staticNested = nestedComponents.filter(n => !n.isDynamic || n.isPropDerived)
1035
1042
 
1036
1043
  // Handle nested components
1037
1044
  if (staticNested.length > 0) {
@@ -1266,6 +1273,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
1266
1273
  result.push({
1267
1274
  ...loop.childComponent,
1268
1275
  isDynamic: !loop.isStaticArray,
1276
+ isPropDerived: !!loop.isPropDerivedArray,
1269
1277
  })
1270
1278
  }
1271
1279
  }
@@ -2471,7 +2479,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2471
2479
 
2472
2480
  literal(value: string | number | boolean | null, literalType: LiteralType): string {
2473
2481
  if (literalType === 'string') return `"${value}"`
2474
- if (literalType === 'null') return '""'
2482
+ if (literalType === 'null') return 'nil'
2475
2483
  return String(value)
2476
2484
  }
2477
2485
 
@@ -2530,8 +2538,8 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2530
2538
  if (result) return result
2531
2539
  }
2532
2540
 
2533
- // find().property → {{with bf_find ...}}{{.Property}}{{end}}
2534
- if (object.kind === 'higher-order' && object.method === 'find') {
2541
+ // find().property / findLast().property → {{with bf_find ...}}{{.Property}}{{end}}
2542
+ if (object.kind === 'higher-order' && (object.method === 'find' || object.method === 'findLast')) {
2535
2543
  const findResult = this.renderHigherOrderExpr(object, emit)
2536
2544
  if (findResult) {
2537
2545
  return `{{with ${findResult}}}{{.${this.capitalizeFieldName(property)}}}{{end}}`
@@ -2613,6 +2621,12 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2613
2621
  return `or ${wrapLeft} ${wrapRight}`
2614
2622
  }
2615
2623
 
2624
+ // Note: JSX-level ternaries (`{expr ? a : b}`) are handled at the
2625
+ // IR level as IRConditional, which goes through convertConditionToGo
2626
+ // → renderConditionExpr (preamble-aware). This emitter method is
2627
+ // only reached for ternaries nested inside other ParsedExpr trees
2628
+ // (e.g. template-literal interpolation), where the test is always a
2629
+ // simple pipeline expression (runtime helpers, not template blocks).
2616
2630
  conditional(
2617
2631
  test: ParsedExpr,
2618
2632
  consequent: ParsedExpr,
@@ -2620,8 +2634,6 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2620
2634
  emit: (e: ParsedExpr) => string,
2621
2635
  ): string {
2622
2636
  const t = emit(test)
2623
- // Nested conditionals already return complete {{if}}...{{end}} blocks;
2624
- // literals return bare text (used within attributes).
2625
2637
  const c = this.renderConditionalBranch(consequent)
2626
2638
  const a = this.renderConditionalBranch(alternate)
2627
2639
  return `{{if ${t}}}${c}{{else}}${a}{{end}}`
@@ -2672,7 +2684,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2672
2684
  const reconstructed = { kind: 'higher-order' as const, method, object, param, predicate }
2673
2685
  const result = this.renderHigherOrderExpr(reconstructed, emit)
2674
2686
  if (result) return result
2675
- if (method === 'find' || method === 'findIndex') {
2687
+ if (method === 'find' || method === 'findIndex' || method === 'findLast' || method === 'findLastIndex') {
2676
2688
  const templateBlock = this.renderFindTemplateBlock(reconstructed, emit)
2677
2689
  if (templateBlock) return templateBlock
2678
2690
  }
@@ -2941,26 +2953,29 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2941
2953
  return `bf_filter ${arrayExpr} "${field}" ${value}`
2942
2954
  }
2943
2955
 
2944
- if (expr.method === 'find' || expr.method === 'findIndex') {
2956
+ if (expr.method === 'find' || expr.method === 'findIndex' || expr.method === 'findLast' || expr.method === 'findLastIndex') {
2945
2957
  const eqPred = this.extractEqualityPredicate(
2946
2958
  expr.predicate, expr.param, e => this.renderParsedExpr(e)
2947
2959
  )
2948
2960
  if (!eqPred) return null
2949
- const func = expr.method === 'find' ? 'bf_find' : 'bf_find_index'
2950
- return `${func} ${arrayExpr} "${eqPred.field}" ${eqPred.value}`
2961
+ const funcMap: Record<string, string> = {
2962
+ find: 'bf_find', findIndex: 'bf_find_index',
2963
+ findLast: 'bf_find_last', findLastIndex: 'bf_find_last_index',
2964
+ }
2965
+ return `${funcMap[expr.method]} ${arrayExpr} "${eqPred.field}" ${eqPred.value}`
2951
2966
  }
2952
2967
 
2953
2968
  return null
2954
2969
  }
2955
2970
 
2956
2971
  /**
2957
- * Render find()/findIndex() with complex predicates using {{range}}{{if}}...{{break}} blocks.
2958
- * Falls back from bf_find/bf_find_index when extractEqualityPredicate returns null.
2959
- * Reuses renderFilterExpr for condition rendering.
2972
+ * Render find/findIndex/findLast/findLastIndex with complex predicates
2973
+ * using range/if blocks. Falls back from bf_find/bf_find_last helpers
2974
+ * when extractEqualityPredicate returns null.
2960
2975
  *
2961
- * @param expr - The higher-order find/findIndex expression
2962
- * @param renderArray - Function to render the array expression
2963
- * @param propertyAccess - Optional property to access on the found element (for find().property)
2976
+ * find/findIndex use break on first match (forward scan).
2977
+ * findLast/findLastIndex iterate forward and keep overwriting a result
2978
+ * variable; the final value is the last match.
2964
2979
  */
2965
2980
  private renderFindTemplateBlock(
2966
2981
  expr: Extract<ParsedExpr, { kind: 'higher-order' }>,
@@ -2980,6 +2995,17 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
2980
2995
  return `{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{$i}}{{break}}{{end}}{{end}}`
2981
2996
  }
2982
2997
 
2998
+ if (expr.method === 'findLast') {
2999
+ const v = `$bf_r${this.templateVarCounter++}`
3000
+ const capture = propertyAccess ? `.${propertyAccess}` : '.'
3001
+ return `{{${v} := ""}}{{range ${arrayExpr}}}{{if ${condition}}}{{${v} = ${capture}}}{{end}}{{end}}{{${v}}}`
3002
+ }
3003
+
3004
+ if (expr.method === 'findLastIndex') {
3005
+ const v = `$bf_r${this.templateVarCounter++}`
3006
+ return `{{${v} := -1}}{{range $i, $_ := ${arrayExpr}}}{{if ${condition}}}{{${v} = $i}}{{end}}{{end}}{{${v}}}`
3007
+ }
3008
+
2983
3009
  return null
2984
3010
  }
2985
3011
 
@@ -3003,13 +3029,14 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3003
3029
  if (condition.includes('[UNSUPPORTED')) return null
3004
3030
 
3005
3031
  if (expr.method === 'every') {
3006
- // Negate condition: if NOT condition, set false and break
3032
+ const v = `$bf_r${this.templateVarCounter++}`
3007
3033
  const negated = this.negateGoCondition(condition)
3008
- return `{{$bf_result := true}}{{range ${arrayExpr}}}{{if ${negated}}}{{$bf_result = false}}{{break}}{{end}}{{end}}{{$bf_result}}`
3034
+ return `{{${v} := true}}{{range ${arrayExpr}}}{{if ${negated}}}{{${v} = false}}{{break}}{{end}}{{end}}{{${v}}}`
3009
3035
  }
3010
3036
 
3011
3037
  if (expr.method === 'some') {
3012
- return `{{$bf_result := false}}{{range ${arrayExpr}}}{{if ${condition}}}{{$bf_result = true}}{{break}}{{end}}{{end}}{{$bf_result}}`
3038
+ const v = `$bf_r${this.templateVarCounter++}`
3039
+ return `{{${v} := false}}{{range ${arrayExpr}}}{{if ${condition}}}{{${v} = true}}{{break}}{{end}}{{end}}{{${v}}}`
3013
3040
  }
3014
3041
 
3015
3042
  return null
@@ -3068,6 +3095,28 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3068
3095
  return expr.kind === 'logical' || expr.kind === 'unary' || expr.kind === 'conditional'
3069
3096
  }
3070
3097
 
3098
+ /**
3099
+ * Split a rendered template block into preamble + final expression.
3100
+ * The last `{{...}}` must be a variable reference (`$bf_rN` or
3101
+ * `$bf_result`). Control tokens like `{{end}}` or `{{break}}` are
3102
+ * rejected — those template blocks (e.g. find's range/break form)
3103
+ * can't be composed in binary/logical expressions.
3104
+ */
3105
+ private splitPreamble(rendered: string): { preamble: string; expr: string } | null {
3106
+ if (!rendered.includes('{{')) return null
3107
+ const lastOpen = rendered.lastIndexOf('{{')
3108
+ const lastClose = rendered.lastIndexOf('}}')
3109
+ if (lastOpen >= 0 && lastClose > lastOpen) {
3110
+ const candidate = rendered.substring(lastOpen + 2, lastClose)
3111
+ if (!candidate.startsWith('$')) return null
3112
+ return {
3113
+ preamble: rendered.substring(0, lastOpen),
3114
+ expr: candidate,
3115
+ }
3116
+ }
3117
+ return null
3118
+ }
3119
+
3071
3120
  // =============================================================================
3072
3121
  // Block Body Condition Rendering
3073
3122
  // =============================================================================
@@ -3310,7 +3359,7 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3310
3359
  return `"${expr.value}"`
3311
3360
  }
3312
3361
  if (expr.literalType === 'null') {
3313
- return '""'
3362
+ return 'nil'
3314
3363
  }
3315
3364
  return String(expr.value)
3316
3365
 
@@ -3682,196 +3731,149 @@ export class GoTemplateAdapter extends BaseAdapter implements ParsedExprEmitter,
3682
3731
  return { condition: `false`, preamble: '' }
3683
3732
  }
3684
3733
 
3685
- const rendered = this.renderConditionExpr(parsed)
3686
-
3687
- // Detect template blocks (e.g., from every/some with complex predicates).
3688
- // These cannot be placed inside {{if ...}} directly.
3689
- // Split into preamble (template block) + condition variable.
3690
- if (rendered.startsWith('{{')) {
3691
- const lastOpen = rendered.lastIndexOf('{{')
3692
- const lastClose = rendered.lastIndexOf('}}')
3693
- if (lastOpen >= 0 && lastClose > lastOpen) {
3694
- const preamble = rendered.substring(0, lastOpen)
3695
- const condition = rendered.substring(lastOpen + 2, lastClose)
3696
- return { condition, preamble }
3697
- }
3698
- }
3699
-
3700
- return { condition: rendered, preamble: '' }
3734
+ const { preamble, expr: condition } = this.renderConditionExpr(parsed)
3735
+ return { condition, preamble }
3701
3736
  }
3702
3737
 
3703
- /**
3704
- * Render a ParsedExpr as a Go template condition.
3705
- */
3706
- private renderConditionExpr(expr: ParsedExpr): string {
3738
+ private renderConditionExpr(expr: ParsedExpr): { preamble: string; expr: string } {
3739
+ const plain = (e: string) => ({ preamble: '', expr: e })
3740
+
3707
3741
  switch (expr.kind) {
3708
3742
  case 'identifier':
3709
- // Inside a `{{range $_, $todo := .Todos}}` loop, a bare reference
3710
- // to the loop variable (`todo`) is just Go template's dot. The
3711
- // `ParsedExprEmitter` path already handles this at memberAccess
3712
- // (line ~2449); this condition-expression path needs the same
3713
- // normalization or `todo.done` ends up as `.Todo.Done` — a
3714
- // non-existent field that Go template silently expands to ""
3715
- // and then aborts the surrounding `{{if}}`/template execution
3716
- // (echo logs it as a 200 with truncated bytes; #1442 repro).
3717
3743
  {
3718
3744
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
3719
3745
  if (currentLoopParam && expr.name === currentLoopParam) {
3720
- return '.'
3746
+ return plain('.')
3721
3747
  }
3722
3748
  }
3723
- return `.${this.capitalizeFieldName(expr.name)}`
3749
+ return plain(`.${this.capitalizeFieldName(expr.name)}`)
3724
3750
 
3725
3751
  case 'literal':
3726
- if (expr.literalType === 'string') {
3727
- return `"${expr.value}"`
3728
- }
3729
- if (expr.literalType === 'null') {
3730
- return '""'
3731
- }
3732
- return String(expr.value)
3752
+ if (expr.literalType === 'string') return plain(`"${expr.value}"`)
3753
+ if (expr.literalType === 'null') return plain('nil')
3754
+ return plain(String(expr.value))
3733
3755
 
3734
3756
  case 'call': {
3735
- // Signal call: count() -> .Count
3736
3757
  if (expr.callee.kind === 'identifier' && expr.args.length === 0) {
3737
- return `.${this.capitalizeFieldName(expr.callee.name)}`
3758
+ return plain(`.${this.capitalizeFieldName(expr.callee.name)}`)
3738
3759
  }
3739
- return this.renderParsedExpr(expr)
3760
+ return plain(this.renderParsedExpr(expr))
3740
3761
  }
3741
3762
 
3742
3763
  case 'member': {
3743
- // Handle .length with higher-order filter → len (bf_filter ...)
3744
3764
  if (expr.property === 'length' && expr.object.kind === 'higher-order') {
3745
- const result = this.renderFilterLengthExpr(expr.object, e => this.renderConditionExpr(e))
3746
- if (result) {
3747
- return result
3748
- }
3765
+ // renderFilterLengthExpr uses bf_filter runtime helpers (not
3766
+ // template blocks), so .preamble is always empty here today.
3767
+ // If a future higher-order method produces preambles through
3768
+ // this path, the callback would need to propagate them.
3769
+ const result = this.renderFilterLengthExpr(expr.object, e => this.renderConditionExpr(e).expr)
3770
+ if (result) return plain(result)
3749
3771
  }
3750
3772
 
3751
- // Handle SolidJS-style props pattern: props.xxx -> .Xxx
3752
3773
  if (expr.object.kind === 'identifier' && this.propsObjectName && expr.object.name === this.propsObjectName) {
3753
- return `.${this.capitalizeFieldName(expr.property)}`
3774
+ return plain(`.${this.capitalizeFieldName(expr.property)}`)
3754
3775
  }
3755
3776
 
3756
- // Loop-param member access: `todo.done` inside
3757
- // `{{range $_, $todo := .Todos}}` is `.Done` (Go template's dot
3758
- // is the current item). The `ParsedExprEmitter` already does
3759
- // this for renderParsedExpr; mirror it here so condition-only
3760
- // positions like boolean attributes (`checked={todo.done}`)
3761
- // and `{{if}}` operands don't fall through to the generic
3762
- // `.Todo.Done` shape, which references a non-existent field.
3763
3777
  {
3764
3778
  const currentLoopParam = this.loopParamStack[this.loopParamStack.length - 1]
3765
3779
  if (expr.object.kind === 'identifier' && currentLoopParam && expr.object.name === currentLoopParam) {
3766
- return `.${this.capitalizeFieldName(expr.property)}`
3780
+ return plain(`.${this.capitalizeFieldName(expr.property)}`)
3767
3781
  }
3768
3782
  }
3769
3783
 
3770
3784
  const obj = this.renderConditionExpr(expr.object)
3771
3785
  if (expr.property === 'length') {
3772
- return `len ${obj}`
3786
+ return { preamble: obj.preamble, expr: `len ${obj.expr}` }
3773
3787
  }
3774
- return `${obj}.${this.capitalizeFieldName(expr.property)}`
3788
+ return { preamble: obj.preamble, expr: `${obj.expr}.${this.capitalizeFieldName(expr.property)}` }
3775
3789
  }
3776
3790
 
3777
3791
  case 'binary': {
3778
- // Check if left operand needs parentheses (e.g., function calls in Go template)
3779
3792
  const leftNeedsParens = this.needsParensInGoTemplate(expr.left)
3780
- let left = this.renderConditionExpr(expr.left)
3781
- if (leftNeedsParens) {
3782
- left = `(${left})`
3783
- }
3793
+ const leftResult = this.renderConditionExpr(expr.left)
3794
+ const left = leftNeedsParens ? `(${leftResult.expr})` : leftResult.expr
3784
3795
 
3785
3796
  const rightNeedsParens = this.needsParensInGoTemplate(expr.right)
3786
- let right = this.renderConditionExpr(expr.right)
3787
- if (rightNeedsParens) {
3788
- right = `(${right})`
3789
- }
3797
+ const rightResult = this.renderConditionExpr(expr.right)
3798
+ const right = rightNeedsParens ? `(${rightResult.expr})` : rightResult.expr
3790
3799
 
3800
+ const preamble = leftResult.preamble + rightResult.preamble
3801
+
3802
+ let result: string
3791
3803
  switch (expr.op) {
3792
3804
  case '===':
3793
3805
  case '==':
3794
- return `eq ${left} ${right}`
3806
+ result = `eq ${left} ${right}`; break
3795
3807
  case '!==':
3796
3808
  case '!=':
3797
- return `ne ${left} ${right}`
3809
+ result = `ne ${left} ${right}`; break
3798
3810
  case '>':
3799
- return `gt ${left} ${right}`
3811
+ result = `gt ${left} ${right}`; break
3800
3812
  case '<':
3801
- return `lt ${left} ${right}`
3813
+ result = `lt ${left} ${right}`; break
3802
3814
  case '>=':
3803
- return `ge ${left} ${right}`
3815
+ result = `ge ${left} ${right}`; break
3804
3816
  case '<=':
3805
- return `le ${left} ${right}`
3806
- // Arithmetic in conditions
3817
+ result = `le ${left} ${right}`; break
3807
3818
  case '+':
3808
- return `bf_add ${left} ${right}`
3819
+ result = `bf_add ${left} ${right}`; break
3809
3820
  case '-':
3810
- return `bf_sub ${left} ${right}`
3821
+ result = `bf_sub ${left} ${right}`; break
3811
3822
  case '*':
3812
- return `bf_mul ${left} ${right}`
3823
+ result = `bf_mul ${left} ${right}`; break
3813
3824
  case '/':
3814
- return `bf_div ${left} ${right}`
3825
+ result = `bf_div ${left} ${right}`; break
3815
3826
  default:
3816
- return `${left} ${expr.op} ${right}`
3827
+ result = `${left} ${expr.op} ${right}`
3817
3828
  }
3829
+ return { preamble, expr: result }
3818
3830
  }
3819
3831
 
3820
3832
  case 'unary': {
3821
3833
  const arg = this.renderConditionExpr(expr.argument)
3822
- if (expr.op === '!') {
3823
- return `not ${arg}`
3824
- }
3825
- if (expr.op === '-') {
3826
- return `bf_neg ${arg}`
3827
- }
3834
+ if (expr.op === '!') return { preamble: arg.preamble, expr: `not ${arg.expr}` }
3835
+ if (expr.op === '-') return { preamble: arg.preamble, expr: `bf_neg ${arg.expr}` }
3828
3836
  return arg
3829
3837
  }
3830
3838
 
3831
3839
  case 'logical': {
3832
- const left = this.renderConditionExpr(expr.left)
3833
- const right = this.renderConditionExpr(expr.right)
3834
- // Wrap in parentheses if needed
3835
- const wrapLeft = this.needsParens(expr.left) ? `(${left})` : left
3836
- const wrapRight = this.needsParens(expr.right) ? `(${right})` : right
3837
- if (expr.op === '&&') {
3838
- return `and ${wrapLeft} ${wrapRight}`
3839
- }
3840
- return `or ${wrapLeft} ${wrapRight}`
3840
+ const leftResult = this.renderConditionExpr(expr.left)
3841
+ const rightResult = this.renderConditionExpr(expr.right)
3842
+ const preamble = leftResult.preamble + rightResult.preamble
3843
+ const wrapLeft = this.needsParens(expr.left) ? `(${leftResult.expr})` : leftResult.expr
3844
+ const wrapRight = this.needsParens(expr.right) ? `(${rightResult.expr})` : rightResult.expr
3845
+ const result = expr.op === '&&'
3846
+ ? `and ${wrapLeft} ${wrapRight}`
3847
+ : `or ${wrapLeft} ${wrapRight}`
3848
+ return { preamble, expr: result }
3841
3849
  }
3842
3850
 
3843
3851
  case 'conditional': {
3844
- // Ternary in condition: (cond ? a : b) is unusual but handle it
3845
3852
  const test = this.renderConditionExpr(expr.test)
3846
- return test // Just return the test part for condition context
3853
+ return test
3847
3854
  }
3848
3855
 
3849
3856
  case 'template-literal':
3850
- // Template literals as conditions are unusual
3851
- return this.renderParsedExpr(expr)
3857
+ return plain(this.renderParsedExpr(expr))
3852
3858
 
3853
3859
  case 'arrow-fn':
3854
- // Arrow functions shouldn't appear in conditions
3855
- return '[ARROW-FN]'
3860
+ return plain('[ARROW-FN]')
3856
3861
 
3857
- case 'higher-order':
3858
- // Higher-order methods in conditions need special handling
3859
- return this.renderParsedExpr(expr)
3862
+ case 'higher-order': {
3863
+ const rendered = this.renderParsedExpr(expr)
3864
+ const split = this.splitPreamble(rendered)
3865
+ if (split) return split
3866
+ return plain(rendered)
3867
+ }
3860
3868
 
3861
3869
  case 'array-literal':
3862
- // Array literals in conditions have no Go template form —
3863
- // delegate to renderParsedExpr so the `arrayLiteral` BF101
3864
- // gate fires consistently with non-condition positions.
3865
- return this.renderParsedExpr(expr)
3870
+ return plain(this.renderParsedExpr(expr))
3866
3871
 
3867
3872
  case 'array-method':
3868
- // Same delegation pattern — `arrayMethod` records the
3869
- // refusal diagnostic at one site rather than duplicating it
3870
- // for condition-position emission.
3871
- return this.renderParsedExpr(expr)
3873
+ return plain(this.renderParsedExpr(expr))
3872
3874
 
3873
3875
  case 'unsupported':
3874
- return expr.raw
3876
+ return plain(expr.raw)
3875
3877
  }
3876
3878
  }
3877
3879