Skip to content

fix(vim.eval): evaluate Vim bool to Python bool, make num_to_str faster#603

Merged
justinmk merged 1 commit intoneovim:masterfrom
brianhuster:vbool
Jan 28, 2026
Merged

fix(vim.eval): evaluate Vim bool to Python bool, make num_to_str faster#603
justinmk merged 1 commit intoneovim:masterfrom
brianhuster:vbool

Conversation

@brianhuster
Copy link
Copy Markdown
Contributor

@brianhuster brianhuster commented Jan 11, 2026

Problem:

  • In Nvim, Vim bool is evaled to string, but in Vim 8+, it is evaluated to
    Python bool.
  • Previously, the argument against compatibility with Vim 8+ in this
    aspect is that adding checking for Python bool would be slower. But if
    we use type(obj) with operator is, it would be 3% ~ 80% faster
    than the current num_to_str implementation (more complex obj seems to
    have more performance gain).

This is the script I use to benchmark

import timeit

num_types = (int, float)

def num_to_str(obj):
    if isinstance(obj, num_types):
        return str(obj)
    else:
        return obj

def num_to_str2(obj):
    obj_type = type(obj)
    if obj_type is int or obj_type is float:
        return str(obj)
    else:
        return obj

def num_to_str3(obj):
    obj_type = type(obj)
    if obj_type == int or obj_type == float:
        return str(obj)
    else:
        return

test_cases = [
    (100, "Integer"),
    (10.5, "Float"),
    (True, "Boolean"),
    ("hello", "String"),
    ([1, 2], "List"),
    ({'a': 1, 'b': 2}, "Dict"),  # Restored!
]

def run_benchmark():
    # Using a fixed format string for perfect alignment
    fmt = "{:<15} | {:<10} | {:<18} | {:<18} | {:>9}"
    header = fmt.format("Input", "Type", "isinstance (s)",
                        "type is (s)", "type == (s)")

    print("\n" + header)
    print("-" * len(header))

    for val, label in test_cases:
        # Number of loops: 1,000,000
        t1 = timeit.timeit("num_to_str(obj)",
                           number=1_000_000, globals={'obj': val, **globals()})
        t2 = timeit.timeit("num_to_str2(obj)",
                           number=1_000_000, globals={'obj': val, **globals()})
        t3 = timeit.timeit("num_to_str3(obj)",
                           number=1_000_000, globals={'obj': val, **globals()})

        display_val = str(val)
        if len(display_val) > 14:
            display_val = display_val[:11] + "..."

        print(fmt.format(display_val, label, f"{t1:.4f}",
                         f"{t2:.4f}", f"{t3:.4f}"))

if __name__ == "__main__":
    run_benchmark()

And the benchmark result:

Input           | Type       | isinstance (s)     | type is (s)        | type == (s)
------------------------------------------------------------------------------------
100             | Integer    | 0.1125             | 0.1095             |    0.1118
10.5            | Float      | 0.3256             | 0.2512             |    0.2791
True            | Boolean    | 0.0731             | 0.0604             |    0.0788
hello           | String     | 0.1005             | 0.0600             |    0.0793
[1, 2]          | List       | 0.0940             | 0.0602             |    0.0755
{'a': 1, 'b...  | Dict       | 0.1080             | 0.0599             |    0.0759

So clearly using type(obj) with is is the fastest way.

Solution:

  • Use type(obj) with is operator is to check for exact number

@justinmk

@brianhuster brianhuster force-pushed the vbool branch 2 times, most recently from dc388c4 to b4f9e01 Compare January 11, 2026 13:11
@brianhuster brianhuster changed the title fix: Vim boolean should be vim.eval to Python bool fix: Vim boolean should be "vim.eval" to Python bool Jan 11, 2026
Problem:
- In Nvim, Vim bool is evaled to string, but in Vim 8+, it is evaled to
  Python bool. So pynvim's legacy vim.eval is not compatible with Vim
  8+.
- Previously, the argument against compatibility with Vim 8+ in this
  aspect is that adding checking for Python bool would be slower. But if
  we use `type(obj)` with operator `is`, it would be 3% ~ 80% faster
  than the current num_to_str implementation (more complex obj seems to
  have more performance gain).

This is the script I use to benchmark
```python
import timeit

num_types = (int, float)

def num_to_str(obj):
    if isinstance(obj, num_types):
        return str(obj)
    else:
        return obj

def num_to_str2(obj):
    obj_type = type(obj)
    if obj_type is int or obj_type is float:
        return str(obj)
    else:
        return obj

def num_to_str3(obj):
    obj_type = type(obj)
    if obj_type == int or obj_type == float:
        return str(obj)
    else:
        return

test_cases = [
    (100, "Integer"),
    (10.5, "Float"),
    (True, "Boolean"),
    ("hello", "String"),
    ([1, 2], "List"),
    ({'a': 1, 'b': 2}, "Dict"),  # Restored!
]

def run_benchmark():
    # Using a fixed format string for perfect alignment
    fmt = "{:<15} | {:<10} | {:<18} | {:<18} | {:>9}"
    header = fmt.format("Input", "Type", "isinstance (s)",
                        "type is (s)", "type == (s)")

    print("\n" + header)
    print("-" * len(header))

    for val, label in test_cases:
        setup = "from __main__ import num_to_str, num_to_str2, num_types"

        # Number of loops: 1,000,000
        t1 = timeit.timeit("num_to_str(obj)", setup=setup,
                           number=1_000_000, globals={'obj': val, **globals()})
        t2 = timeit.timeit("num_to_str2(obj)", setup=setup,
                           number=1_000_000, globals={'obj': val, **globals()})
        t3 = timeit.timeit("num_to_str3(obj)", setup=setup,
                           number=1_000_000, globals={'obj': val, **globals()})

        display_val = str(val)
        if len(display_val) > 14:
            display_val = display_val[:11] + "..."

        print(fmt.format(display_val, label, f"{t1:.4f}",
                         f"{t2:.4f}", f"{t3:.4f}"))

if __name__ == "__main__":
    run_benchmark()
```

And the benchmark result:

```
Input           | Type       | isinstance (s)     | type is (s)        | type == (s)
------------------------------------------------------------------------------------
100             | Integer    | 0.1125             | 0.1095             |    0.1118
10.5            | Float      | 0.3256             | 0.2512             |    0.2791
True            | Boolean    | 0.0731             | 0.0604             |    0.0788
hello           | String     | 0.1005             | 0.0600             |    0.0793
[1, 2]          | List       | 0.0940             | 0.0602             |    0.0755
{'a': 1, 'b...  | Dict       | 0.1080             | 0.0599             |    0.0759
```

So clearly using `type(obj)` with `is` is the fastest way.

Solution:
- Use type(obj) with is operator is to check for exact number
@brianhuster brianhuster changed the title fix: Vim boolean should be "vim.eval" to Python bool fix(vim.eval): Vim boolean should be evaluated to Python bool Jan 11, 2026
@brianhuster brianhuster changed the title fix(vim.eval): Vim boolean should be evaluated to Python bool fix(vim.eval): evaluate Vim bool to Python bool, make num_to_str faster Jan 11, 2026
@brianhuster brianhuster changed the title fix(vim.eval): evaluate Vim bool to Python bool, make num_to_str faster fix(vim.eval): evaluate Vim bool to Python bool, also make vim.eval faster Jan 11, 2026
@brianhuster brianhuster changed the title fix(vim.eval): evaluate Vim bool to Python bool, also make vim.eval faster fix(vim.eval): evaluate Vim bool to Python bool, make num_to_str faster Jan 11, 2026
@justinmk
Copy link
Copy Markdown
Member

In Nvim, Vim bool is evaled to string, but in Vim 8+, it is evaluated to Python bool.

@wookayin mentioned in #523 this was technically a breaking change, but if Vim also made a similar change then seems acceptable.

@wookayin
Copy link
Copy Markdown
Member

I agree we should making breaking changes for correct types.

@brianhuster
Copy link
Copy Markdown
Contributor Author

brianhuster commented Jan 28, 2026

Btw, I'd like to say the main reason I make this PR: I have never found the explanation in #523 (comment) persuasive.

This is Vim 7.3 document

Evaluates the expression str using the vim internal expression
evaluator (see |expression|). Returns the expression result as:
(1) a string if the Vim expression evaluates to a string or number

By "vim internal expression evaluator", I am sure the doc means Vimscript evaluator and not Python one. I don't know enough about Vimscript internal, but if it evaluates to a number then vim.eval should return string "0" or "1" instead of "True" or "False". If it just evaluates to a real boolean then that should be undefined behavior and we cannot use Vim 7.3 document to explain it anymore

by (1) and that booleans are integers in Python, vim.eval('v:true') should evaluate to the Python string "True" and not the boolean True.

But this part of the comment seems to mistake Vimscript evaluator with Python evaluator. Vim document only says that Vim number (which is the result of the "vim internal expression evaluator") must be converted to Python string, not that Python number must be converted to Python string (imagine in the future Vim could add a rule that Vim funcref will be vim.evaled to Python integer, that will not contradict any existing Vim 7.3 document)

@justinmk justinmk merged commit a06146f into neovim:master Jan 28, 2026
33 of 50 checks passed
@brianhuster brianhuster deleted the vbool branch January 29, 2026 02:13
@wookayin wookayin added this to the 0.7.0 milestone Feb 6, 2026
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.

3 participants