Skip to content

Commit

Permalink
Basic associative array support.
Browse files Browse the repository at this point in the history
Able to get through more of 'bash_completion' like this.  It doesn't
crash.

Added a spec test to show off declaration, putting, and getting.

Still need:
- associative array literals
- pretty printing with ASDL
  • Loading branch information
Andy Chu committed Sep 22, 2018
1 parent 8ebfc25 commit 83237b7
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 46 deletions.
6 changes: 6 additions & 0 deletions asdl/asdl_.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,11 @@ def __repr__(self):
return '<Bool>'


class DictType(_RuntimeType):
def __repr__(self):
return '<Dict>'


class ArrayType(_RuntimeType):
def __init__(self, desc):
self.desc = desc
Expand Down Expand Up @@ -138,6 +143,7 @@ def LookupFieldType(self, field_name):
'string': StrType(),
'int': IntType(),
'bool': BoolType(),
'dict': DictType(),
}


Expand Down
3 changes: 3 additions & 0 deletions asdl/format.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,9 @@ def MakeFieldSubtree(obj, field_name, desc, abbrev_hook, omit_empty=True):
elif isinstance(desc, asdl.BoolType):
out_val = _ColoredString('T' if field_val else 'F', _OTHER_LITERAL)

elif isinstance(desc, asdl.DictType):
raise AssertionError

elif isinstance(desc, asdl.Sum) and asdl.is_simple(desc):
out_val = field_val.name

Expand Down
22 changes: 13 additions & 9 deletions core/cmd_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -397,18 +397,23 @@ def _CheckStatus(self, status, node, argv0=None):
'[%d] %r exited with status %d', os.getpid(),
node.__class__.__name__, status, status=status)

def _EvalLhs(self, node, spid):
def _EvalLhs(self, node, spid, lookup_mode):
"""lhs_expr -> lvalue."""
assert isinstance(node, ast.lhs_expr), node

if node.tag == lhs_expr_e.LhsName: # a=x
node = runtime.LhsName(node.name)
node.spids.append(spid)
return node
runtime_node = runtime.LhsName(node.name)
runtime_node.spids.append(spid)
return runtime_node

if node.tag == lhs_expr_e.LhsIndexedName: # a[1+2]=x
i = self.arith_ev.Eval(node.index)
runtime_node = runtime.LhsIndexedName(node.name, i)
# TODO: Look up node.name and check if the cell is AssocArray, or if the
# type is AssocArray.
int_coerce = not self.mem.IsAssocArray(node.name, lookup_mode)
#log('int_coerce %s', int_coerce)
index = self.arith_ev.Eval(node.index, int_coerce=int_coerce)

runtime_node = runtime.LhsIndexedName(node.name, index)
runtime_node.spids.append(node.spids[0]) # copy left-most token over
return runtime_node

Expand Down Expand Up @@ -731,8 +736,6 @@ def _Dispatch(self, node, fork_external):
elif node.keyword in (Id.Assign_Declare, Id.Assign_Typeset):
# declare is like local, except it can also be used outside functions?
lookup_mode = scope_e.LocalOnly
# TODO: Respect flags. -r and -x matter, but -a and -A might be
# implicit in the RHS?
elif node.keyword == Id.Assign_Readonly:
lookup_mode = scope_e.Dynamic
flags.append(var_flags_e.ReadOnly)
Expand Down Expand Up @@ -763,7 +766,7 @@ def _Dispatch(self, node, fork_external):

else: # plain assignment
spid = pair.spids[0] # Source location for tracing
lval = self._EvalLhs(pair.lhs, spid)
lval = self._EvalLhs(pair.lhs, spid, lookup_mode)

# RHS can be a string or array.
if pair.rhs:
Expand All @@ -776,6 +779,7 @@ def _Dispatch(self, node, fork_external):
# NOTE: In bash and mksh, declare -a myarray makes an empty cell with
# Undef value, but the 'array' attribute.

#log('setting %s to %s with flags %s', lval, val, flags)
self.mem.SetVar(lval, val, flags, lookup_mode,
strict_array=self.exec_opts.strict_array)

Expand Down
3 changes: 2 additions & 1 deletion core/comp_builtins.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ def Complete(argv, ex, funcs, comp_lookup):

commands = arg_r.Rest()
if not commands:
raise args.UsageError('missing required commands')
comp_lookup.PrintSpecs()
return 0

for command in commands:
# NOTE: bash doesn't actually check the name until completion time, but
Expand Down
8 changes: 8 additions & 0 deletions core/completion.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,14 @@ def __init__(self):
# searched linearly.
self.patterns = []

def PrintSpecs(self):
for name in sorted(self.lookup):
print('%s %s' % (name, self.lookup[name]))
print('---')
print('%s' % self.empty_comp)
print('%s' % self.first_comp)
print('%s' % self.patterns)

def RegisterName(self, name, chain):
"""
Called by 'complete' builtin.
Expand Down
37 changes: 29 additions & 8 deletions core/expr_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ def EvalLhs(node, arith_ev, mem, exec_opts):
"""
#log('lhs_expr NODE %s', node)
assert isinstance(node, ast.lhs_expr), node

if node.tag == lhs_expr_e.LhsName: # a = b
# Problem: It can't be an array?
# a=(1 2)
Expand Down Expand Up @@ -203,25 +204,45 @@ def EvalLhs(node, arith_ev, mem, exec_opts):
return val, lval


def _ValToArith(val, word=None):
def _ValToArith(val, int_coerce=True, word=None):
"""Convert runtime.value to a Python int or list of strings."""
assert isinstance(val, runtime.value), '%r %r' % (val, type(val))

if int_coerce:
if val.tag == value_e.Undef: # 'nounset' already handled before got here
warn('converting undefined variable to 0')
return 0

if val.tag == value_e.Str:
return _StringToInteger(val.s, word=word) # may raise FatalRuntimeError

if val.tag == value_e.StrArray: # array is valid on RHS, but not on left
return val.strs

if val.tag == value_e.AssocArray:
return val.d

raise AssertionError(val)

if val.tag == value_e.Undef: # 'nounset' already handled before got here
return 0
return '' # I think nounset is handled elsewhere

if val.tag == value_e.Str:
return _StringToInteger(val.s, word=word) # may raise FatalRuntimeError
return val.s

if val.tag == value_e.StrArray: # array is valid on RHS, but not on left
return val.strs

if val.tag == value_e.AssocArray:
return val.d


class ArithEvaluator(_ExprEvaluator):

def _ValToArithOrError(self, val, word=None):
def _ValToArithOrError(self, val, int_coerce=True, word=None):
try:
i = _ValToArith(val, word=word)
i = _ValToArith(val, int_coerce=int_coerce, word=word)

except util.FatalRuntimeError as e:
if self.exec_opts.strict_arith:
raise
Expand Down Expand Up @@ -250,7 +271,7 @@ def _Store(self, lval, new_int):
val = runtime.Str(str(new_int))
self.mem.SetVar(lval, val, (), scope_e.Dynamic)

def Eval(self, node):
def Eval(self, node, int_coerce=True):
"""
Args:
node: osh_ast.arith_expr
Expand All @@ -264,12 +285,12 @@ def Eval(self, node):

if node.tag == arith_expr_e.ArithVarRef: # $(( x )) (can be array)
val = self._LookupVar(node.name)
return self._ValToArithOrError(val)
return self._ValToArithOrError(val, int_coerce=int_coerce)

# $(( $x )) or $(( ${x}${y} )), etc.
if node.tag == arith_expr_e.ArithWord:
val = self.word_ev.EvalWordToString(node.w)
return self._ValToArithOrError(val, word=node.w)
return self._ValToArithOrError(val, int_coerce=int_coerce, word=node.w)

if node.tag == arith_expr_e.UnaryAssign: # a++
op_id = node.op_id
Expand Down
3 changes: 2 additions & 1 deletion core/runtime.asdl
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,14 @@ module runtime
Undef
| Str(string s)
| StrArray(string* strs)
| AssocArray(dict d)

-- For Oil?
-- | ArrayInt(int* array_int)
-- | ArrayBool(bool* a)

-- For storing a variable.
cell = (value val, bool exported, bool readonly)
cell = (value val, bool exported, bool readonly, bool is_assoc_array)

-- An undefined variable can become an indexed array with s[x]=1. But if we
-- 'declare -A' it, it will be undefined and waiting to turn into an
Expand Down
79 changes: 58 additions & 21 deletions core/state.py
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,20 @@ def _FindCellAndNamespace(self, name, lookup_mode, writing=True):
else:
raise AssertionError(lookup_mode)

def IsAssocArray(self, name, lookup_mode):
"""Returns whether a name resolve to a cell with an associative array.
We need to know this to evaluate the index expression properly -- should it
be coerced to an integer or not?
"""
cell, _ = self._FindCellAndNamespace(name, lookup_mode)
if cell:
if cell.val.tag == value_e.AssocArray: # foo=([key]=value)
return True
if cell.is_assoc_array: # declare -A
return True
return False

def SetVar(self, lval, value, new_flags, lookup_mode, strict_array=False):
"""
Args:
Expand Down Expand Up @@ -769,13 +783,16 @@ def SetVar(self, lval, value, new_flags, lookup_mode, strict_array=False):
cell.exported = True
if var_flags_e.ReadOnly in new_flags:
cell.readonly = True
if var_flags_e.AssocArray in new_flags:
cell.is_assoc_array = True
else:
if value is None:
# set -o nounset; local foo; echo $foo # It's still undefined!
value = runtime.Undef() # export foo, readonly foo
cell = runtime.cell(value,
var_flags_e.Exported in new_flags ,
var_flags_e.ReadOnly in new_flags )
var_flags_e.Exported in new_flags,
var_flags_e.ReadOnly in new_flags,
var_flags_e.AssocArray in new_flags)
namespace[lval.name] = cell

if (cell.val is not None and cell.val.tag == value_e.StrArray and
Expand Down Expand Up @@ -803,8 +820,9 @@ def SetVar(self, lval, value, new_flags, lookup_mode, strict_array=False):
# bash/mksh have annoying behavior of letting you do LHS assignment to
# Undef, which then turns into an array. (Undef means that set -o
# nounset fails.)
if (cell.val.tag == value_e.Str or
(cell.val.tag == value_e.Undef and strict_array)):
cell_tag = cell.val.tag
if (cell_tag == value_e.Str or
(cell_tag == value_e.Undef and strict_array)):
# s=x
# s[1]=y # invalid
e_die("Entries in value of type %s can't be assigned to",
Expand All @@ -813,22 +831,31 @@ def SetVar(self, lval, value, new_flags, lookup_mode, strict_array=False):
if cell.readonly:
e_die("Can't assign to readonly value", span_id=left_spid)

if cell.val.tag == value_e.Undef:
self._BindNewArrayWithEntry(namespace, lval, value, new_flags)
if cell_tag == value_e.Undef:
if cell.is_assoc_array:
self._BindNewAssocArrayWithEntry(namespace, lval, value, new_flags)
else:
self._BindNewArrayWithEntry(namespace, lval, value, new_flags)
return

strs = cell.val.strs
try:
strs[lval.index] = value.s
except IndexError:
# Fill it in with None. It could look like this:
# ['1', 2, 3, None, None, '4', None]
# Then ${#a[@]} counts the entries that are not None.
#
# TODO: strict-array for Oil arrays won't auto-fill.
n = lval.index - len(strs) + 1
strs.extend([None] * n)
strs[lval.index] = value.s
if cell_tag == value_e.StrArray:
strs = cell.val.strs
try:
strs[lval.index] = value.s
except IndexError:
# Fill it in with None. It could look like this:
# ['1', 2, 3, None, None, '4', None]
# Then ${#a[@]} counts the entries that are not None.
#
# TODO: strict-array for Oil arrays won't auto-fill.
n = lval.index - len(strs) + 1
strs.extend([None] * n)
strs[lval.index] = value.s
return

if cell_tag == value_e.AssocArray:
cell.val.d[lval.index] = value.s
return

else:
raise AssertionError(lval.__class__.__name__)
Expand All @@ -838,9 +865,19 @@ def _BindNewArrayWithEntry(self, namespace, lval, value, new_flags):
items = [None] * lval.index
items.append(value.s)
new_value = runtime.StrArray(items)
# arrays can't be exported
cell = runtime.cell(new_value, False, var_flags_e.ReadOnly in new_flags)
namespace[lval.name] = cell

# arrays can't be exported; can't have AssocArray flag
readonly = var_flags_e.ReadOnly in new_flags
namespace[lval.name] = runtime.cell(new_value, False, readonly, False)

def _BindNewAssocArrayWithEntry(self, namespace, lval, value, new_flags):
"""Fill 'namespace' with a new indexed array entry."""
d = {lval.index: value.s} # TODO: RHS has to be string?
new_value = runtime.AssocArray(d)

# associative arrays can't be exported; don't need AssocArray flag
readonly = var_flags_e.ReadOnly in new_flags
namespace[lval.name] = runtime.cell(new_value, False, readonly, False)

def InternalSetGlobal(self, name, new_val):
"""For setting read-only globals internally.
Expand Down
2 changes: 0 additions & 2 deletions core/word_compile.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,8 +110,6 @@ def ParseAssignFlags(flag_args):
flags.append(var_flags_e.Exported)
elif char == 'r':
flags.append(var_flags_e.ReadOnly)
elif char == 'a':
flags.append(var_flags_e.Array)
elif char == 'A':
flags.append(var_flags_e.AssocArray)
else:
Expand Down
6 changes: 5 additions & 1 deletion core/word_eval.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,13 @@ def _ValueToPartValue(val, quoted):
elif val.tag == value_e.StrArray:
return runtime.ArrayPartValue(val.strs)

elif val.tag == value_e.AssocArray:
# TODO: Is this correct?
return runtime.ArrayPartValue(val.d.values())

else:
# Undef should be caught by _EmptyStrOrError().
raise AssertionError
raise AssertionError(val.__class__.__name__)


def _MakeWordFrames(part_vals):
Expand Down
6 changes: 6 additions & 0 deletions spec/assoc.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
declare -A a
a=([aa]=b [foo]=bar ['a+1']=c)

#### create empty assoc array, put, then get
declare -A d # still undefined
d['foo']=bar
echo ${d['foo']}
## stdout: bar

#### retrieve indices with !
declare -A a
a=([aa]=b [foo]=bar ['a+1']=c)
Expand Down
7 changes: 4 additions & 3 deletions test/spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -516,12 +516,13 @@ append() {
$BASH $MKSH $OSH_LIST "$@"
}

# associative array -- mksh implements different associative arrays.
# associative array -- mksh and zsh implement different associative arrays.
assoc() {
sh-spec spec/assoc.test.sh $BASH "$@"
sh-spec spec/assoc.test.sh --osh-failures-allowed 10 \
$BASH $OSH_LIST "$@"
}

# ZSH also has associative arrays, which means we probably need them
# ZSH also has associative arrays
assoc-zsh() {
sh-spec spec/assoc-zsh.test.sh $ZSH "$@"
}
Expand Down

0 comments on commit 83237b7

Please sign in to comment.