On 30-Apr-15, Bram Moolenaar wrote:
>
> Olaf Dabrunz wrote:
>
> > Example:
> >
> > :let d = {'a': 99, 'b': 100}
> > :lockvar 1 d
> > :call extend(d, {'a': 123})
> > E741: Value is locked: extend() argument
> > :let d.a = 123
> > :let d
> > d {'a': 123, 'b': 100}
> >
> > The dictionary is locked, but the 'a' item is not, so extend() on 'a'
> > should succeed in the same way as the 'let' does. According to ':he
> > lockvar', a dictionary lock protects against item deletion and addition,
> > but not against changing an existing item.
>
> Hmm, one could argue that extend() adds the new key/value pair, thus
> that it should fail.
For lists, I agree -- extend(list, list2[, where]) does not touch
existing items.
For dicts, extend() modifies existing items and adds new items when
their keys do not exist yet.
> Zyx's argument that it may change one existing key/value but then fail
> on a following one that adds a key makes sense.
This was also discussed by you and ZyX for patch 7.4.419 ("when part of
a list is locked it's possible to make changes"), which is a related
situation. ZyX proposed several approaches, and the result was "[list]
items will be removed [or changed] only if all items are not locked".
> Perhaps we should just
> state "extend() on a locked list or dict does not work".
If we let extend() fail here, then ':let' (and map()) should fail as
well. The lock should not have a different effect when the change is
attempted by different means.
This would be a different lock model.
The all-or-nothing semantics you and ZyX mention can be implemented for
extend(dict, dict2[, how]) as well. But note:
- extend(dict, dict2[, how]) currently already has several
exit-on-error conditions:
- adding a function to scopes l: or g: that conflicts with a
builtin function
- adding or replacing an item in a variable scope, when the key
is not a valid variable name
- changing an existing dict item while 'how' is 'error'
- changing a locked dict item (since 7.4.698).
For all-or-nothing semantics, all of these need to be moved into a
separate checking loop.
(This is the reason why I considered all-or-nothing semantics
beyond the scope of my patch: the current implementation has
exit-on-error semantics.)
- A separate checking loop needs to repeat all dictionary lookups.
For my tests, this made extend(dict, ...) take twice as much time
to complete.
:let d = {}
:for i in range(10000) | let d[i] = 't'.i | endfor
:for t in range(3)
: let start = reltime()
: for i in range(10000) | call extend(d, d) | endfor
: echomsg reltimestr(reltime(start))
:endfor
Unpatched:
3.255444
3.243517
3.248523
Old patch (exit-on-error semantics):
3.249932
3.256253
3.255438
New patch with separate checking loop (see below):
6.452288
6.390474
6.420924
I am not sure if taking twice as long is a good tradeoff for
having all-or-nothing semantics.
- In case of an out of memory error, extend() skips that item and
continues with the next one (continue-after-error semantics).
For OOM, all-or-nothing semantics can only be achieved by
implementing a rollback, because OOM can only happen when we
actually execute the changes.
A rollback requires copies of the changed items. Making them is
relatively expensive.
Note that list functions do not implement this: 'extend(list,
list2, how)' and ':let list[i:j] = list2' and probably others do
have exit-on-OOM semantics.
One reason for not implementing all-or-nothing for OOM may be that
OOM rarely happens: as discussed in another thread, users tend to
terminate other processes before OOM (to avoid a slow system that
is swapping or thrashing), or an OOM-killer may kill some other
process to avoid OOM.
It may not be worth the slowdown just to have a rollback on OOM.
An updated patch that implements all-or-nothing semantics in
extend(dict, dict2[, how]) (except for OOM) is at the end of this mail.
On another note, while looking at the 7.4.419 patch again, I realized
that it adds checks to ':unlet' for item locks before it deletes any
items. This is not needed under the locking model described in ':he
lockvar': item locks protect against item change, not against item
deletion.
The list lock, which protects against deletion, is checked before
already.
Here is a patch that removes the unneeded item lock checks. Note that
no changes to the tests are needed, as the list lock already makes sure
the items cannot be deleted.
diff -r 18d84ed365a5 src/eval.c
--- a/src/eval.c Wed Apr 22 22:18:22 2015 +0200
+++ b/src/eval.c Fri May 01 09:50:23 2015 +0200
@@ -3669,15 +3676,6 @@ do_unlet_var(lp, name_end, forceit)
listitem_T *ll_li = lp->ll_li;
int ll_n1 = lp->ll_n1;
- while (ll_li != NULL && (lp->ll_empty2 || lp->ll_n2 >= ll_n1))
- {
- li = ll_li->li_next;
- if (tv_check_lock(ll_li->li_tv.v_lock, lp->ll_name, FALSE))
- return FAIL;
- ll_li = li;
- ++ll_n1;
- }
-
/* Delete a range of List items. */
while (lp->ll_li != NULL && (lp->ll_empty2 || lp->ll_n2 >= lp->ll_n1))
{
Here is the updated extend() patch with all-or-nothing semantics:
diff -r 18d84ed365a5 src/eval.c
--- a/src/eval.c Wed Apr 22 22:18:22 2015 +0200
+++ b/src/eval.c Sat May 02 02:57:53 2015 +0200
@@ -10502,6 +10502,10 @@ dict_extend(d1, d2, action)
int todo;
char_u *arg_errmsg = (char_u *)N_("extend() argument");
+ /*
+ * Check whether the operation can complete without errors (other than
+ * OOM).
+ */
todo = (int)d2->dv_hashtab.ht_used;
for (hi2 = d2->dv_hashtab.ht_array; todo > 0; ++hi2)
{
@@ -10518,26 +10522,45 @@ dict_extend(d1, d2, action)
&& HI2DI(hi2)->di_tv.v_type == VAR_FUNC
&& var_check_func_name(hi2->hi_key,
di1 == NULL))
- break;
+ return;
if (!valid_varname(hi2->hi_key))
- break;
- }
+ return;
+ }
+ if (di1 == NULL)
+ {
+ if (tv_check_lock(d1->dv_lock, arg_errmsg, TRUE))
+ return;
+ }
+ else if (*action == 'e')
+ {
+ EMSG2(_("E737: Key already exists: %s"), hi2->hi_key);
+ return;
+ }
+ else if (*action == 'f' && HI2DI(hi2) != di1
+ && (tv_check_lock(di1->di_tv.v_lock, arg_errmsg, TRUE)
+ || var_check_ro(di1->di_flags, arg_errmsg, TRUE)))
+ return;
+ }
+ }
+
+ /*
+ * Replace or add dict items.
+ */
+ todo = (int)d2->dv_hashtab.ht_used;
+ for (hi2 = d2->dv_hashtab.ht_array; todo > 0; ++hi2)
+ {
+ if (!HASHITEM_EMPTY(hi2))
+ {
+ --todo;
+ di1 = dict_find(d1, hi2->hi_key, -1);
if (di1 == NULL)
{
di1 = dictitem_copy(HI2DI(hi2));
if (di1 != NULL && dict_add(d1, di1) == FAIL)
dictitem_free(di1);
}
- else if (*action == 'e')
- {
- EMSG2(_("E737: Key already exists: %s"), hi2->hi_key);
- break;
- }
else if (*action == 'f' && HI2DI(hi2) != di1)
{
- if (tv_check_lock(di1->di_tv.v_lock, arg_errmsg, TRUE)
- || var_check_ro(di1->di_flags, arg_errmsg, TRUE))
- break;
clear_tv(&di1->di_tv);
copy_tv(&HI2DI(hi2)->di_tv, &di1->di_tv);
}
@@ -10601,8 +10624,7 @@ f_extend(argvars, rettv)
d1 = argvars[0].vval.v_dict;
d2 = argvars[1].vval.v_dict;
- if (d1 != NULL && !tv_check_lock(d1->dv_lock, arg_errmsg, TRUE)
- && d2 != NULL)
+ if (d1 != NULL && d2 != NULL)
{
/* Check the third argument. */
if (argvars[2].v_type != VAR_UNKNOWN)
diff -r 18d84ed365a5 src/testdir/test55.in
--- a/src/testdir/test55.in Wed Apr 22 22:18:22 2015 +0200
+++ b/src/testdir/test55.in Sat May 02 02:57:53 2015 +0200
@@ -408,6 +408,27 @@ let l = [0, 1, 2, 3]
:endtry
:$put =string(d)
:"
+:$put ='extend() after lock on dict:'
+:unlet! d
+:let d = {'a': 99, 'b': 100, 'c': 101}
+:lockvar 1 d
+:try
+: $put =string(extend(d, {'a': 123}))
+: $put ='ok: did extend() existing item'
+: $put =string(extend(d, {'x': 'new'}))
+: $put ='wrong: did extend() with new item'
+:catch
+: $put =v:exception[:14]
+:endtry
+:$put =string(d)
+:try
+: $put =string(extend(d, {'a': 789, 'x': 'new', 'b': 42, 'c': 52}))
+:catch
+: $put =v:exception[:14]
+:endtry
+:$put =string(d)
+:unlet d
+:"
:$put ='No remove() of write-protected scope-level variable:'
:fun! Tfunc(this_is_a_loooooooooong_parameter_name)
: try
diff -r 18d84ed365a5 src/testdir/test55.ok
--- a/src/testdir/test55.ok Wed Apr 22 22:18:22 2015 +0200
+++ b/src/testdir/test55.ok Sat May 02 02:57:53 2015 +0200
@@ -138,6 +138,13 @@ did map()
No extend() after lock on dict item:
Vim(put):E741:
{'a': 99, 'b': 100}
+extend() after lock on dict:
+{'a': 123, 'b': 100, 'c': 101}
+ok: did extend() existing item
+Vim(put):E741:
+{'a': 123, 'b': 100, 'c': 101}
+Vim(put):E741:
+{'a': 123, 'b': 100, 'c': 101}
No remove() of write-protected scope-level variable:
Vim(put):E795:
No extend() of write-protected scope-level variable:
--
Olaf Dabrunz (oda <at> fctrace.org)
--
--
You received this message from the "vim_dev" maillist.
Do not top-post! Type your reply below the text you are replying to.
For more information, visit http://www.vim.org/maillist.php
---
You received this message because you are subscribed to the Google Groups
"vim_dev" group.
To unsubscribe from this group and stop receiving emails from it, send an email
to [email protected].
For more options, visit https://groups.google.com/d/optout.