Skip to content

refactor: Reuse FilteredValueToken and fix architectural layering#878

Draft
rosomri wants to merge 20 commits into
harttle:masterfrom
rosomri:inner_expression_parentheses_proposal
Draft

refactor: Reuse FilteredValueToken and fix architectural layering#878
rosomri wants to merge 20 commits into
harttle:masterfrom
rosomri:inner_expression_parentheses_proposal

Conversation

@rosomri
Copy link
Copy Markdown
Contributor

@rosomri rosomri commented Apr 14, 2026

Summary

This PR addresses the architectural concerns raised in review comments on #863 by:

  1. Reusing the existing FilteredValueToken instead of creating a duplicate GroupedExpressionToken
  2. Fixing the layering violation where tokens depended on templates (Filter instances)
  3. Moving filter resolution from parse time to render time

This is a proposed architectural improvement built on top of #863 for discussion.

Architectural Changes

1. Reuse FilteredValueToken instead of GroupedExpressionToken

Before:

readGroupOrRange(): { type: 'range', range: RangeToken } | { type: 'groupedExpression', groupedExpression: GroupedExpressionToken }

After:

readGroupOrRange(): FilteredValueToken | RangeToken

This eliminates code duplication and leverages the existing FilteredValueToken class that was already designed for this purpose, as suggested in review comments.

2. Fix Layering Violation

Problem: Tokens had resolvedFilters?: Filter[], causing tokens to depend on the template layer (violation of tokens → render → templates hierarchy).

Solution:

  • Removed resolvedFilters from FilteredValueToken
  • Added liquid reference to Context for runtime filter resolution
  • Build Filter instances at render time in evalFilteredValueToken()

Before:

export class FilteredValueToken extends Token {
  public resolvedFilters?: Filter[]  // ❌ Token depends on template layer
}

After:

export class FilteredValueToken extends Token {
  // Only has FilterToken[] - proper layering maintained ✅
}

3. Lazy Filter Resolution (Render Time)

Before: Filters were resolved at parse time via resolveGroupedExpressionFilters():

// In tag constructors
resolveGroupedExpressionFilters(this.collection, liquid)

After: Filters are resolved lazily at render time:

function * evalFilteredValueToken (token: FilteredValueToken, ctx: Context, lenient: boolean) {
  assert(ctx.liquid, 'FilteredValueToken evaluation requires liquid instance in context')
  let val = yield token.initial.evaluate(ctx, lenient)
  
  for (const filterToken of token.filters) {
    const filterImpl = ctx.liquid.filters[filterToken.name]
    const filter = new Filter(filterToken, filterImpl, ctx.liquid)
    val = yield filter.render(val, ctx)
  }
  
  return val
}

Changes by File

Core Architecture

  • src/context/context.ts: Added liquid?: any reference to Context for runtime filter resolution
  • src/liquid.ts: Pass liquid: this when creating Context instances
  • src/tokens/filtered-value-token.ts: Removed resolvedFilters (layering fix)
  • src/render/expression.ts: Build Filter instances at render time in evalFilteredValueToken()

Simplification

  • src/parser/tokenizer.ts: Simplified readGroupOrRange() to return FilteredValueToken | RangeToken
  • src/template/value.ts: Removed resolveGroupedExpressionFilters() function entirely
  • src/tags/case.ts, for.ts, tablerow.ts: Removed explicit resolveGroupedExpressionFilters() calls

Cleanup

  • src/tokens/grouped-expression-token.ts: Deleted (replaced by FilteredValueToken)
  • src/parser/token-kind.ts: Marked GroupedExpression as deprecated
  • src/util/type-guards.ts: Updated isGroupedExpressionToken() as deprecated alias

Test Results

✅ All 1537 tests pass

Test Suites: 86 passed, 86 total
Tests:       1537 passed, 1537 total

Manual Testing

All grouped expression use cases work correctly:

  • ✓ Basic filtered comparison: (foo | upcase) == "BAR"
  • ✓ Both sides filtered: (a | upcase) == (b | upcase)
  • ✓ For loop with filtered range: (1..(items | size))
  • ✓ Case with filtered value: (status | downcase)
  • ✓ Regular range syntax: (1..3) (backward compatible)
  • ✓ Chained filters: (name | downcase | size)
  • ✓ Nested grouped expressions: ((foo | append: "!") | upcase)

Benefits

  1. Proper Layering: Tokens → Render → Templates (no backward dependencies)
  2. Code Reuse: Eliminated duplicate GroupedExpressionToken class
  3. Lazy Evaluation: Filters resolved at render time, not parse time
  4. Simpler Code: Removed recursive resolveGroupedExpressionFilters() function
  5. Backward Compatible: All existing tests pass

Question for Review

This implementation follows @jg-rp's suggestion to add liquid to Context. The alternative mentioned in the PR comments was to pass Liquid to the Tokenizer constructor, but that would require changes to filter runtime tokenization support.

Does the approach of adding liquid to Context align with your architectural vision? Or would you prefer the Tokenizer constructor approach?

Builds on #863

skynetigor and others added 15 commits March 23, 2026 17:27
…l-and-Loop-Tags' of https://github.com/skynetigor/liquidjs into 833_Support-Value-Expressions-as-Operands-in-Conditional-and-Loop-Tags

Made-with: Cursor

# Conflicts:
#	src/parser/tokenizer.ts
…l-and-Loop-Tags' of https://github.com/skynetigor/liquidjs into 833_Support-Value-Expressions-as-Operands-in-Conditional-and-Loop-Tags
…cenarios for enabled and disabled grouped expressions in case, for, if, unless tags, ensuring proper handling of expressions and error throwing for invalid syntax.
The readGroupedExpression() test suite was duplicated twice in the spec file. Removed the duplicate block to avoid redundant test execution.
Extract inline grouped expression variable extraction logic into a dedicated
function for consistency with other extractors (extractFilteredValueVariables,
extractPropertyAccessVariable).

This addresses PR harttle#863 comment 7 - improves code organization and
maintainability.
collection: ValueToken | GroupedExpressionToken

Addresses PR harttle#863 comment 5.
…lters

Addresses PR review comments 4, 6, 8, 9 - moves grouped expression evaluation
from parse-time resolution to render-time lazy evaluation following the
generator-based async/sync duality pattern used throughout liquidjs.

Key changes:
- Replace resolvedValue (Value instance) with resolvedFilters (Filter[])
- Rename resolveGroupedExpressions() to resolveGroupedExpressionFilters()
- Move evaluation logic to evalGroupedExpressionToken() at render time
- Build Filter instances at parse time (carry liquid reference for render)
- Evaluate expression and apply filters lazily via generators
- Add support for tablerow tag with grouped expressions
- Remove duplicate getFilter() method in Value class

Maintains proper layering (tokens → render → templates) and consistency
with Value.value() pattern. Filter resolution still happens at parse time
since it requires liquid.filters access, but actual evaluation is deferred
to render time.

Tags that store raw ValueToken (for, case when-values, tablerow) still need
explicit resolveGroupedExpressionFilters() calls. Tags that wrap with
new Value() get automatic recursive resolution via Value constructor.
Replace GroupedExpressionToken with existing FilteredValueToken to avoid
code duplication and fix layering violation where tokens depended on
templates (Filter instances).

Key changes:
- Reuse FilteredValueToken instead of GroupedExpressionToken
- Simplify readGroupOrRange() to return FilteredValueToken | RangeToken
- Add liquid reference to Context for runtime filter resolution
- Build Filter instances at render time in evalFilteredValueToken()
- Remove resolveGroupedExpressionFilters() and parse-time resolution
- Remove explicit resolution calls from tag constructors

This maintains proper architectural layering (tokens → render → templates)
with no backward dependencies, as requested in PR review feedback.

All 1537 tests pass.
@rosomri rosomri marked this pull request as draft April 14, 2026 07:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants