Skip to content

Commit 4991783

Browse files
authored
[osh] Implement InitializerList, e.g. a=(v [k]=v [k]+=v) (#2279)
* [osh/word_parse] Introduce InitializerLiteral for `(v [k]=v [k]+=v ...)` * [osh/word_parse] Allow mixed forms of initializer in BashAssocLiteral We allow mixed forms of initializer such as (1 2 [3]=4). We extend the element type of "BashAssocLiteral.pairs" to "InitializerWord", which is either "ArrayWord" or "AssocPair". "ArrayWord" is the non array-assignment form of a word. In addition, "AssocPair" now records also whether the word in initializer has the form "[]=" or "[]+=". * [refactor frontend/syntax] Rename "{BashAssoc => Initializer}Literal" * [osh/cmd_eval] Process InitializerList for tempenv * [core/bash_impl] Implement InitializerList_ToStrForShellPrint for xtrace * [refactor frontend/id_kind_def] Rename "Id.Right_{ShArrayLiteral => Initializer}" * [osh] Add temporary adjustment for "assoc=(1 2)" The current OSH behavior of overwriting "assoc" with a new indexed array for "assoc=(1 2)" conflicts with the Bash 5.1 feature "assoc=(key value)". The current master behavior will soon be changed when the Bash 5.1 feature is implemented, yet we keep the current master behavior for now to avoid changing the behavior too many times. This commit adjusts the behavior to be backward compatible with the current master version. * [spec] Update spec tests for "arr=([index]=value)" * [spec/assoc] Update spec tests for "assoc=([key]=value)" We now support the sparse-array initialization of the form "arr=([index]=value)", so an attempt of assigning an initializer list to an indexed array no longer overwrites the original array with BashAssoc. With this change, the expected behaviors of three spec tests in spec/assoc.test.sh are changed. We adjust the expected results of the three tests and copy them into spec/ble-sparse.test.sh. We also leave BashAssoc versions of spec tests in spec/assoc.test.sh. * [spec/ysh-printing] Update spec tests for "assoc=([key]=value)" * [spec/assign-extended] Confirm that "declare -p sp" is correctly parsed The initialier-list implementation resolves the inconsistency in the sparse array representation of the form "declare -a sp=([2]=v)". This commit removes the "oils_failures_allowed" introduced in Ref. [1] (as a part of PR 2257, which turned on the sparse array representation of indexed arrays). [1] bfa7497#diff-097d35f191fa3ada9a05d61d3020a2b3acb43e800141b67021094fb05d8e769e * [spec/assoc] Reduce "oils_failures_allowed" OSH now implements the feature required by spec/assoc#37 "Implicit increment of keys" so passes the test. * [doc] Update reference * [doc/{ref/{chap-type-method,toc-osh},known-differences,quirks}] Update * [refactor] Rename "{Sh => Ysh}ArrayLiteral"
1 parent 28d6024 commit 4991783

31 files changed

+935
-246
lines changed

builtin/assign_osh.py

+84-31
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from core import bash_impl
1616
from core import error
17-
from core.error import e_usage
17+
from core.error import e_usage, e_die
1818
from core import state
1919
from core import vm
2020
from display import ui
@@ -193,8 +193,9 @@ def _PrintVariables(mem, cmd_val, attrs, print_flags, builtin=_OTHER):
193193
return 1
194194

195195

196-
def _AssignVarForBuiltin(mem, rval, pair, which_scopes, flags):
197-
# type: (Mem, value_t, AssignArg, scope_t, int) -> None
196+
def _AssignVarForBuiltin(mem, rval, pair, which_scopes, flags, arith_ev,
197+
flag_a, flag_A):
198+
# type: (Mem, value_t, AssignArg, scope_t, int, sh_expr_eval.ArithEvaluator, bool, bool) -> None
198199
"""For 'export', 'readonly', and NewVar to respect += and flags.
199200
200201
Like 'setvar' (scope_e.LocalOnly), unless dynamic scope is on. That is, it
@@ -203,28 +204,85 @@ def _AssignVarForBuiltin(mem, rval, pair, which_scopes, flags):
203204
Used for assignment builtins, (( a = b )), {fd}>out, ${x=}, etc.
204205
"""
205206
lval = LeftName(pair.var_name, pair.blame_word)
206-
if pair.plus_eq:
207+
208+
initializer = None # type: value.InitializerList
209+
if rval is None:
210+
# When 'export e+=', then rval is value.Str('')
211+
# When 'export foo', the pair.plus_eq flag is false.
212+
# Thus, when rval is None, plus_eq cannot be True.
213+
assert not pair.plus_eq, pair.plus_eq
214+
# NOTE: when rval is None, only flags are changed
215+
val = None # type: value_t
216+
elif rval.tag() == value_e.InitializerList:
217+
old_val = sh_expr_eval.OldValue(
218+
lval,
219+
mem,
220+
None, # ignore set -u
221+
pair.blame_word)
222+
initializer = cast(value.InitializerList, rval)
223+
224+
# OSH compatibility: "declare -A assoc=(1 2)" is a usage
225+
# error. This code will soon be removed when the
226+
# initialization of the form "assoc=(key value)" is supported.
227+
if (flag_A and len(initializer.assigns) > 0 and
228+
initializer.assigns[0].key is None):
229+
e_usage("Can't initialize assoc array with indexed array",
230+
pair.blame_word)
231+
232+
val = old_val
233+
if flag_a:
234+
if old_val.tag() == value_e.BashAssoc:
235+
# Note: OSH allows overwriting an existing BashArray with an
236+
# empty BashArray. Bash does not allow this.
237+
val = bash_impl.BashArray_New()
238+
elif old_val.tag() in (value_e.Undef, value_e.Str,
239+
value_e.BashArray):
240+
# We do not need adjustemnts for -a. These types are
241+
# consistently handled within ListInitialize
242+
pass
243+
else:
244+
e_die(
245+
"Can't convert type %s into BashArray" %
246+
ui.ValType(old_val), pair.blame_word)
247+
elif flag_A:
248+
if old_val.tag() in (value_e.Undef, value_e.Str,
249+
value_e.BashArray):
250+
# Note: We explicitly initialize BashAssoc for Undef.
251+
# Note: OSH allows overwriting an existing BashArray with an
252+
# empty BashAssoc. Bash does not allow this.
253+
val = bash_impl.BashAssoc_New()
254+
elif old_val.tag() == value_e.BashAssoc:
255+
# We do not need adjustments for -A.
256+
pass
257+
else:
258+
e_die(
259+
"Can't convert type %s into BashAssoc" %
260+
ui.ValType(old_val), pair.blame_word)
261+
262+
val = cmd_eval.ListInitializeTarget(val, initializer, pair.plus_eq,
263+
pair.blame_word)
264+
elif pair.plus_eq:
207265
old_val = sh_expr_eval.OldValue(
208266
lval,
209267
mem,
210268
None, # ignore set -u
211269
pair.blame_word)
212-
# When 'export e+=', then rval is value.Str('')
213-
# When 'export foo', the pair.plus_eq flag is false.
214-
assert rval is not None
215270
val = cmd_eval.PlusEquals(old_val, rval)
216271
else:
217-
# NOTE: when rval is None, only flags are changed
218272
val = rval
219273

220274
mem.SetNamed(lval, val, which_scopes, flags=flags)
275+
if initializer is not None:
276+
cmd_eval.ListInitialize(val, initializer, pair.plus_eq,
277+
pair.blame_word, arith_ev)
221278

222279

223280
class Export(vm._AssignBuiltin):
224281

225-
def __init__(self, mem, errfmt):
226-
# type: (Mem, ui.ErrorFormatter) -> None
282+
def __init__(self, mem, arith_ev, errfmt):
283+
# type: (Mem, sh_expr_eval.ArithEvaluator, ui.ErrorFormatter) -> None
227284
self.mem = mem
285+
self.arith_ev = arith_ev
228286
self.errfmt = errfmt
229287

230288
def Run(self, cmd_val):
@@ -266,7 +324,8 @@ def Run(self, cmd_val):
266324
which_scopes = self.mem.ScopesForWriting()
267325
for pair in cmd_val.pairs:
268326
_AssignVarForBuiltin(self.mem, pair.rval, pair, which_scopes,
269-
state.SetExport)
327+
state.SetExport, self.arith_ev, False,
328+
False)
270329

271330
return 0
272331

@@ -298,32 +357,23 @@ def _ReconcileTypes(rval, flag_a, flag_A, pair, mem):
298357
rval = bash_impl.BashAssoc_New()
299358
else:
300359
if flag_a:
301-
if rval.tag() not in (value_e.InternalStringArray,
302-
value_e.BashArray):
303-
e_usage("Got -a but RHS isn't an array",
360+
if rval.tag() != value_e.InitializerList:
361+
e_usage("Got -a but RHS isn't an initializer list",
304362
loc.Word(pair.blame_word))
305-
306363
elif flag_A:
307-
# Special case: declare -A A=() is OK. The () is changed to mean
308-
# an empty associative array.
309-
if rval.tag() == value_e.BashArray:
310-
sparse_val = cast(value.BashArray, rval)
311-
if bash_impl.BashArray_IsEmpty(sparse_val):
312-
return bash_impl.BashAssoc_New()
313-
#return bash_impl.BashArray_New()
314-
315-
if rval.tag() != value_e.BashAssoc:
316-
e_usage("Got -A but RHS isn't an associative array",
364+
if rval.tag() != value_e.InitializerList:
365+
e_usage("Got -A but RHS isn't an initializer list",
317366
loc.Word(pair.blame_word))
318367

319368
return rval
320369

321370

322371
class Readonly(vm._AssignBuiltin):
323372

324-
def __init__(self, mem, errfmt):
325-
# type: (Mem, ui.ErrorFormatter) -> None
373+
def __init__(self, mem, arith_ev, errfmt):
374+
# type: (Mem, sh_expr_eval.ArithEvaluator, ui.ErrorFormatter) -> None
326375
self.mem = mem
376+
self.arith_ev = arith_ev
327377
self.errfmt = errfmt
328378

329379
def Run(self, cmd_val):
@@ -348,19 +398,21 @@ def Run(self, cmd_val):
348398
# - when rval is None, only flags are changed
349399
# - dynamic scope because flags on locals can be changed, etc.
350400
_AssignVarForBuiltin(self.mem, rval, pair, which_scopes,
351-
state.SetReadOnly)
401+
state.SetReadOnly, self.arith_ev, arg.a,
402+
arg.A)
352403

353404
return 0
354405

355406

356407
class NewVar(vm._AssignBuiltin):
357408
"""declare/typeset/local."""
358409

359-
def __init__(self, mem, procs, exec_opts, errfmt):
360-
# type: (Mem, state.Procs, optview.Exec, ui.ErrorFormatter) -> None
410+
def __init__(self, mem, procs, exec_opts, arith_ev, errfmt):
411+
# type: (Mem, state.Procs, optview.Exec, sh_expr_eval.ArithEvaluator, ui.ErrorFormatter) -> None
361412
self.mem = mem
362413
self.procs = procs
363414
self.exec_opts = exec_opts
415+
self.arith_ev = arith_ev
364416
self.errfmt = errfmt
365417

366418
def _PrintFuncs(self, names):
@@ -463,7 +515,8 @@ def Run(self, cmd_val):
463515
for pair in cmd_val.pairs:
464516
rval = _ReconcileTypes(pair.rval, arg.a, arg.A, pair, self.mem)
465517

466-
_AssignVarForBuiltin(self.mem, rval, pair, which_scopes, flags)
518+
_AssignVarForBuiltin(self.mem, rval, pair, which_scopes, flags,
519+
self.arith_ev, arg.a, arg.A)
467520

468521
return status
469522

core/bash_impl.py

+101
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from _devbuild.gen.runtime_asdl import error_code_e, error_code_t
44
from _devbuild.gen.value_asdl import value
5+
from _devbuild.gen.syntax_asdl import loc_t
56

7+
from core.error import e_die
68
from data_lang import j8_lite
79
from mycpp import mops
810
from mycpp import mylib
@@ -30,6 +32,21 @@ def BigInt_LessEq(a, b):
3032
return not mops.Greater(a, b)
3133

3234

35+
class ArrayIndexEvaluator(object):
36+
"""Interface class for implementing the evaluation of array indices in
37+
initializer-list items of the form [i]=1."""
38+
39+
def __init__(self):
40+
# type: () -> None
41+
"""Empty constructor for mycpp."""
42+
pass
43+
44+
def StringToBigInt(self, s, blame_loc):
45+
# type: (str, loc_t) -> mops.BigInt
46+
"""Returns an array index obtained by evaluating the specified string."""
47+
raise NotImplementedError()
48+
49+
3350
#------------------------------------------------------------------------------
3451
# All InternalStringArray operations depending on the internal representation
3552
# of InternalStringArray come here.
@@ -267,6 +284,35 @@ def BashAssoc_New():
267284
return value.BashAssoc(d)
268285

269286

287+
def BashAssoc_Copy(val):
288+
# type: (value.BashAssoc) -> value.BashAssoc
289+
# mycpp limitation: NewDict() needs to be typed
290+
d = mylib.NewDict() # type: Dict[str, str]
291+
for key in val.d:
292+
d[key] = val.d[key]
293+
return value.BashAssoc(d)
294+
295+
296+
def BashAssoc_ListInitialize(val, initializer, has_plus, blame_loc):
297+
# type: (value.BashAssoc, value.InitializerList, bool, loc_t) -> None
298+
299+
if not has_plus:
300+
val.d.clear()
301+
302+
for triplet in initializer.assigns:
303+
if triplet.key is None:
304+
e_die(
305+
"Key is missing. BashAssoc requires a key for %r" %
306+
triplet.rval, blame_loc)
307+
308+
s = triplet.rval
309+
if triplet.plus_eq:
310+
old_s = val.d.get(triplet.key)
311+
if old_s is not None:
312+
s = old_s + s
313+
val.d[triplet.key] = s
314+
315+
270316
def BashAssoc_IsEmpty(assoc_val):
271317
# type: (value.BashAssoc) -> bool
272318
return len(assoc_val.d) == 0
@@ -361,6 +407,14 @@ def BashArray_New():
361407
return value.BashArray(d, max_index)
362408

363409

410+
def BashArray_Copy(val):
411+
# type: (value.BashArray) -> value.BashArray
412+
d = {} # type: Dict[mops.BigInt, str]
413+
for index in val.d:
414+
d[index] = val.d[index]
415+
return value.BashArray(d, val.max_index)
416+
417+
364418
def BashArray_FromList(strs):
365419
# type: (List[str]) -> value.BashArray
366420
d = {} # type: Dict[mops.BigInt, str]
@@ -373,6 +427,30 @@ def BashArray_FromList(strs):
373427
return value.BashArray(d, max_index)
374428

375429

430+
def BashArray_ListInitialize(val, initializer, has_plus, blame_loc, arith_ev):
431+
# type: (value.BashArray, value.InitializerList, bool, loc_t, ArrayIndexEvaluator) -> None
432+
if not has_plus:
433+
val.d.clear()
434+
val.max_index = mops.MINUS_ONE
435+
436+
array_index = val.max_index
437+
for triplet in initializer.assigns:
438+
if triplet.key is not None:
439+
array_index = arith_ev.StringToBigInt(triplet.key, blame_loc)
440+
else:
441+
array_index = mops.Add(array_index, mops.ONE)
442+
443+
s = triplet.rval
444+
if triplet.plus_eq:
445+
old_s = val.d.get(array_index)
446+
if old_s is not None:
447+
s = old_s + s
448+
449+
val.d[array_index] = s
450+
if BigInt_Greater(array_index, val.max_index):
451+
val.max_index = array_index
452+
453+
376454
def BashArray_IsEmpty(sparse_val):
377455
# type: (value.BashArray) -> bool
378456
return len(sparse_val.d) == 0
@@ -505,3 +583,26 @@ def BashArray_ToStrForShellPrint(sparse_val):
505583

506584
body.append(j8_lite.MaybeShellEncode(sparse_val.d[index]))
507585
return "(%s)" % ''.join(body)
586+
587+
588+
#------------------------------------------------------------------------------
589+
# InitializerList operations depending on its internal representation come
590+
# here.
591+
592+
593+
def InitializerList_ToStrForShellPrint(val):
594+
# type: (value.InitializerList) -> str
595+
body = [] # type: List[str]
596+
597+
for init in val.assigns:
598+
if len(body) > 0:
599+
body.append(" ")
600+
if init.key is not None:
601+
key = j8_lite.MaybeShellEncode(init.key)
602+
if init.plus_eq:
603+
body.extend(["[", key, "]+="])
604+
else:
605+
body.extend(["[", key, "]="])
606+
body.append(j8_lite.MaybeShellEncode(init.rval))
607+
608+
return "(%s)" % ''.join(body)

core/dev.py

+4
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ def _PrintShValue(val, buf):
225225
val = cast(value.BashArray, UP_val)
226226
result = bash_impl.BashArray_ToStrForShellPrint(val)
227227

228+
elif case(value_e.InitializerList):
229+
val = cast(value.InitializerList, UP_val)
230+
result = bash_impl.InitializerList_ToStrForShellPrint(val)
231+
228232
buf.write(result)
229233

230234

core/shell.py

+5-4
Original file line numberDiff line numberDiff line change
@@ -211,19 +211,20 @@ def InitAssignmentBuiltins(
211211
mem, # type: state.Mem
212212
procs, # type: state.Procs
213213
exec_opts, # type: optview.Exec
214+
arith_ev, # type: sh_expr_eval.ArithEvaluator
214215
errfmt, # type: ui.ErrorFormatter
215216
):
216217
# type: (...) -> Dict[int, vm._AssignBuiltin]
217218

218219
assign_b = {} # type: Dict[int, vm._AssignBuiltin]
219220

220-
new_var = assign_osh.NewVar(mem, procs, exec_opts, errfmt)
221+
new_var = assign_osh.NewVar(mem, procs, exec_opts, arith_ev, errfmt)
221222
assign_b[builtin_i.declare] = new_var
222223
assign_b[builtin_i.typeset] = new_var
223224
assign_b[builtin_i.local] = new_var
224225

225-
assign_b[builtin_i.export_] = assign_osh.Export(mem, errfmt)
226-
assign_b[builtin_i.readonly] = assign_osh.Readonly(mem, errfmt)
226+
assign_b[builtin_i.export_] = assign_osh.Export(mem, arith_ev, errfmt)
227+
assign_b[builtin_i.readonly] = assign_osh.Readonly(mem, arith_ev, errfmt)
227228

228229
return assign_b
229230

@@ -506,7 +507,7 @@ def Main(
506507
word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, mutable_opts,
507508
tilde_ev, splitter, errfmt)
508509

509-
assign_b = InitAssignmentBuiltins(mem, procs, exec_opts, errfmt)
510+
assign_b = InitAssignmentBuiltins(mem, procs, exec_opts, arith_ev, errfmt)
510511
cmd_ev = cmd_eval.CommandEvaluator(mem, exec_opts, errfmt, procs, assign_b,
511512
arena, cmd_deps, trap_state,
512513
signal_safe)

0 commit comments

Comments
 (0)