vim / vim

The official Vim repository
https://www.vim.org
Vim License
36.62k stars 5.46k forks source link

[vim9script] `typename()` gives incorrect/inconsistent output #13433

Open errael opened 1 year ago

errael commented 1 year ago

Steps to reproduce

This output

l: list<number>
l: list<unknown>
l: list<number>
o: object<Unknown>
o: object<C>

is generated by the following script

vim9script

class C
endclass

var l: list<number>
echo 'l:' typename(l)

l = null_list
# <<< expecte "list<number>" outputs "list<unknown>" # ERROR
echo 'l:' typename(l)

l = []
echo 'l:' typename(l)   # <<< outputs "list<number>"

#l = null_list
#l = ['a']       # <<< this gets "expected list<number>", the type is known

# Similar problem with objects
var o: C
echo 'o:' typename(o)   # output "object<Unknown>" expect "object<C>" # ERROR

o = C.new()
echo 'o:' typename(o)   # as expected: "object<C>"

Expected behaviour

Correct/consistent type information.

l: list<number>
l: list<number>
l: list<number>
o: object<C>
o: object<C>

Version of Vim

9.0.2077

Environment

ubuntu

Logs and stack traces

No response

yegappan commented 1 year ago

When you assign null_list to the variable l, the type of l changes to the type of null_list (which is list). To retain the variable type, but remove the list, you can assign it to an empty list.

var l: list<number> = [1, 2, 3]
l = []
echo typename(l)
errael commented 1 year ago

When you assign null_list to the variable l, the type of l changes to the type of null_list (which is list). To retain the variable type, but remove the list, you can assign it to an empty list.

var l: list<number> = [1, 2, 3]
l = []
echo typename(l)

Note that you get this errror

l: list<number>
l: list<unknown>
Error detected while processing :source buffer=1:
line   12:
E1012: Type mismatch; expected list<number> but got list<string>

from

vim9script

class C
endclass

var l: list<number>
echo 'l:' typename(l)

l = null_list
echo 'l:' typename(l)

l = ['a']

So l does retain the type even as a null_list. The behavior we see, not outputting the correct type information, seems more an implementation artifact that has leaked into programmer visible space.

Assigning [] is not viable; I have code where an empty list is different from a null list.

errael commented 1 year ago

[This is an unworkable solution]

typename() takes an expression, internally a typval_T. The current implementation of a null typval_T has no member type information, like list<number> vs list<string>. This issue only affects list/dict (I think).

In the discussion assume we're dealing with a list.

Here's a possibility on how this issue could be fixed in steps.

  1. Introduce is_null_tv(typeval_T*) function/macro/inline. Use this everywhere that a typval_T is checked to see if it is a null. (Only really needed for dict/list)
  2. Add a flag to typval_T, V_NULL. When set the value is null and there may be type information.
  3. Set V_NULL as possible. For this case, when a variable is evaluated and the result is null, instead of setting v_list to null set it to an empty list and use/clone the type info from the variable.
  4. Use V_NULL as needed.

If there is interest, I'll look at doing this. See below for details/caveats; and it may turn out to be much more difficult or far reaching than anticipated.

Curiously, this is also related to the gazillion null types. If this is implemented, it's more than half way (I think) to a solution to get rid of the plethora of null types, see https://github.com/vim/vim/discussions/13458#discussioncomment-7440006. I do not know if this proposal presents problems relating the current use of "null types".

As with some other issues this one is related to using a builtin function to do something which should be handled as part of the language.


Notes and details of interest to geeks and/or implementers.

I believe this problem only relates to list or dict (and possibly function) because they have a lv_type/dv_type type_S has tt_member for item type. So (1), (2) only needed for list/dict. This means that checks for null value where the item is not a list/dict, e.g. it's a job, do not need to use is_null_tv(), but for uniformity...

The proposal(hack), set a flag in typval_T that means "is_null", and then have an empty list (instead of NULL pointer) that has the complete type information. This would mean that the code that checks if something is null would do

    if ((tv->flags & V_NULL) != 0)  // true if null value

instead of

    if (tv->vval.v_list == NULL)    // true if null value

but would do in some/most situations (at least during a transition period)

    if ((tv->flags & V_NULL) != 0 || tv->vval.v_list == NULL)

The idea is that you could do

    if ((tv->flags & V_NULL) != 0 || tv->vval.v_list == NULL)
    {
        if (tv->vval.v_list != NULL)
        {
            // use the typeinfo associated with the null item.
        }
    }

A singleton/read-only empty-list would be ideal to use for this, but that may not be possible because something might want to reference count it. Note: there is currently no flags field in typeval_T, but one can be added without increasing the size of the struct (think that's true for all CPU arch).

Notice that there's

    actual_type = typval2type(actual_tv, get_copyID(), &type_list,
                                      TVTT_DO_MEMBER | TVTT_MORE_SPECIFIC);

That'll be something interesting to take a look at; including where/how it is used.

NOTE: for this to work, every spot where there's a check for a null vim thing it would need to use is_null_tv(typeval_T*) (for list/dict anyway); once that's in place the V_NULL flag could be used.

Could be a good place to use inline int is_tv_null(typval_T*). I don't know what's acceptable here for vim, might need

#ifdef INLINE_OK
inline int is_tv_null(typval_T*)
#else
#define is_tv_null(x) xxxwhateverxxx
#endif

Or just make it a function and accept the hit.

errael commented 1 year ago

typename() takes an expression, internally a typval_T. The current implementation of a null typval_T has no member type information, like list<number> vs list<string>. This issue only affects list/dict (I think).

A solution could involve saving argument information that can be examined by a builtin function.

errael commented 10 months ago

May be related to #13696