Skip to content

Comparison confusion between false and nil #568

@edev

Description

@edev

liquid-rust version: 0.26.9
rust version: rustc 1.82.0 (f6e511eec 2024-10-15)
OS: Debian 12 (bookworm)

I believe I have found a bug: liquid-rust seems to equate false and nil.

My reasons for believing this is a bug are as follows:

  1. The observed behavior differs from that of Shopify's gem.
  2. The observed behavior surprises me.
  3. The observed behavior is inconsistent (as the code below demonstrates).

I've included Ruby and Rust sample code along with their outputs. I apologize for the long MWEs, but I thought clarity was more important than brevity in this case.

Also, please note that the examples below use copied and pasted templates between Ruby and Rust to ensure consistency; I haven't even touched the template indentation to match both languages' styles.

Shopify (Ruby, using gem liquid version 5.5.1)

require "liquid"

def false_first
  raw_template = \
    "{% if x == true -%}
      if:  true
    {%- elsif x == false -%}
      if: false
    {%- elsif x == nil -%}
      if:   nil
    {%- else -%}
      if:  else
    {%- endif -%}
    ; {% case x -%}
      {%- when true -%}
        case: true
      {%- when false -%}
        case: false
      {%- when nil -%}
        case: nil
      {%- else -%}
        case: else
    {%- endcase %}"

  template = Liquid::Template.parse(raw_template)
  
  puts "=== Comparison order: true, false, nil ==="
  puts "[true]  #{template.render('x' => true)}"
  puts "[false] #{template.render('x' => false)}"
  puts "[nil]   #{template.render('x' => nil)}"
  puts
end

def nil_first
  raw_template = \
    "{% if x == true -%}
      if:  true
    {%- elsif x == nil -%}
      if:   nil
    {%- elsif x == false -%}
      if: false
    {%- else -%}
      if:  else
    {%- endif -%}
    ; {% case x -%}
      {%- when true -%}
        case: true
      {%- when nil -%}
        case: nil
      {%- when false -%}
        case: false
      {%- else -%}
        case: else
    {%- endcase %}"

  template = Liquid::Template.parse(raw_template)
  
  puts "=== Comparison order: true, nil, false ==="
  puts "[true]  #{template.render('x' => true)}"
  puts "[false] #{template.render('x' => false)}"
  puts "[nil]   #{template.render('x' => nil)}"
  puts
end

false_first
nil_first

Ruby output

=== Comparison order: true, false, nil ===
[true]  if:  true; case: true
[false] if: false; case: false
[nil]   if:   nil; case: nil

=== Comparison order: true, nil, false ===
[true]  if:  true; case: true
[false] if: false; case: false
[nil]   if:   nil; case: nil

Rust (using liquid-rust version 0.26.9):

fn main() {
    false_first();
    nil_first();
}

fn false_first() {
    let raw_template =
        "{% if x == true -%}
          if:  true
        {%- elsif x == false -%}
          if: false
        {%- elsif x == nil -%}
          if:   nil
        {%- else -%}
          if:  else
        {%- endif -%}
        ; {% case x -%}
          {%- when true -%}
            case: true
          {%- when false -%}
            case: false
          {%- when nil -%}
            case: nil
          {%- else -%}
            case: else
        {%- endcase %}";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(raw_template)
        .unwrap();

    println!("=== Comparison order: true, false, nil ===");
    println!("[true]        {}", template.render(&liquid::object!({ "x": true })).unwrap());
    println!("[false]       {}", template.render(&liquid::object!({ "x": false })).unwrap());
    println!("[Some(true)]  {}", template.render(&liquid::object!({ "x": Some(true) })).unwrap());
    println!("[Some(false)] {}", template.render(&liquid::object!({ "x": Some(false) })).unwrap());
    println!("[None]        {}", template.render(&liquid::object!({ "x": None::<bool> })).unwrap());
    println!();
}

fn nil_first() {
    let raw_template =
        "{% if x == true -%}
          if:  true
        {%- elsif x == nil -%}
          if:   nil
        {%- elsif x == false -%}
          if: false
        {%- else -%}
          if:  else
        {%- endif -%}
        ; {% case x -%}
          {%- when true -%}
            case: true
          {%- when nil -%}
            case: nil
          {%- when false -%}
            case: false
          {%- else -%}
            case: else
        {%- endcase %}";

    let template = liquid::ParserBuilder::with_stdlib()
        .build()
        .unwrap()
        .parse(raw_template)
        .unwrap();

    println!("=== Comparison order: true, nil, false ===");
    println!("[true]        {}", template.render(&liquid::object!({ "x": true })).unwrap());
    println!("[false]       {}", template.render(&liquid::object!({ "x": false })).unwrap());
    println!("[Some(true)]  {}", template.render(&liquid::object!({ "x": Some(true) })).unwrap());
    println!("[Some(false)] {}", template.render(&liquid::object!({ "x": Some(false) })).unwrap());
    println!("[None]        {}", template.render(&liquid::object!({ "x": None::<bool> })).unwrap());
    println!();
}

Rust output

=== Comparison order: true, false, nil ===
[true]        if:  true; case: true
[false]       if: false; case: false
[Some(true)]  if:  true; case: true
[Some(false)] if: false; case: false
[None]        if: false; case: false

=== Comparison order: true, nil, false ===
[true]        if:  true; case: true
[false]       if:   nil; case: nil
[Some(true)]  if:  true; case: true
[Some(false)] if:   nil; case: nil
[None]        if:   nil; case: nil

Expected output

=== Comparison order: true, false, nil ===
[true]        if:  true; case: true
[false]       if: false; case: false
[Some(true)]  if:  true; case: true
[Some(false)] if: false; case: false
[None]        if:   nil; case: nil

=== Comparison order: true, nil, false ===
[true]        if:  true; case: true
[false]       if: false; case: false
[Some(true)]  if:  true; case: true
[Some(false)] if: false; case: false
[None]        if:   nil; case: nil

Use case

I have an optional Boolean value and need to distinguish Some(true), Some(false), and None.

Workaround

I map the value to a string literal before passing it to liquid::object!, and then I perform string comparison in my template, e.g. "nil" instead of nil.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions