Skip to content

Commit da4c475

Browse files
author
Andy C
committed
[ysh breaking] var and const always create vars in current frame
NOT in the enclosed frame. This was not correct in the initial implementation of closures, around October. It fixes - 3 tests in spec/hay - 1 test in ysh-builtin-eval, i.e. related to Dict (&d). I implemented this by passing 'flags' through to more functions in state::Mem. TODO: I think YSH var const setvar setglobal need separate methods in state::Mem. This should simplify the "executable spec" of YSH too, i.e. "let's make shell/YSH easy to run"
1 parent 89a5196 commit da4c475

File tree

5 files changed

+143
-37
lines changed

5 files changed

+143
-37
lines changed

core/state.py

+46-31
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
ClearExport = 1 << 3
5757
SetNameref = 1 << 4
5858
ClearNameref = 1 << 5
59+
YshDecl = 1 << 6
5960

6061

6162
class ctx_Source(object):
@@ -1217,8 +1218,8 @@ def _Pop(self):
12171218
self.mem.SetNamed(lval, old_val, scope_e.LocalOnly)
12181219

12191220

1220-
def _FrameLookup(frame, name):
1221-
# type: (Dict[str, Cell], str) -> Tuple[Optional[Cell], Dict[str, Cell]]
1221+
def _FrameLookup(frame, name, ysh_decl):
1222+
# type: (Dict[str, Cell], str, bool) -> Tuple[Optional[Cell], Dict[str, Cell]]
12221223
"""
12231224
Look for a name in the frame, then recursively into the enclosing __E__
12241225
frame, if it exists
@@ -1227,13 +1228,14 @@ def _FrameLookup(frame, name):
12271228
if cell:
12281229
return cell, frame
12291230

1230-
rear_cell = frame.get('__E__') # ctx_EnclosedFrame() sets this
1231-
if rear_cell:
1232-
rear_val = rear_cell.val
1233-
assert rear_val, rear_val
1234-
if rear_val.tag() == value_e.Frame:
1235-
to_enclose = cast(value.Frame, rear_val).frame
1236-
return _FrameLookup(to_enclose, name) # recursive call
1231+
if not ysh_decl:
1232+
rear_cell = frame.get('__E__') # ctx_EnclosedFrame() sets this
1233+
if rear_cell:
1234+
rear_val = rear_cell.val
1235+
assert rear_val, rear_val
1236+
if rear_val.tag() == value_e.Frame:
1237+
to_enclose = cast(value.Frame, rear_val).frame
1238+
return _FrameLookup(to_enclose, name, ysh_decl) # recursive call
12371239

12381240
return None, None
12391241

@@ -1719,33 +1721,43 @@ def GetSpecialVar(self, op_id):
17191721
# Named Vars
17201722
#
17211723

1722-
def _ResolveNameOnly(self, name, which_scopes):
1723-
# type: (str, scope_t) -> Tuple[Optional[Cell], Dict[str, Cell]]
1724-
"""Helper for getting and setting variable.
1724+
def _ResolveNameOnly(self, name, which_scopes, flags):
1725+
# type: (str, scope_t, int) -> Tuple[Optional[Cell], Dict[str, Cell]]
1726+
"""Given a variable name, and scope rule, return a Cell and Frame.
1727+
1728+
This function does not resolve 'nameref'. It's used to get and set
1729+
variables.
17251730
17261731
Returns:
1727-
cell: The cell corresponding to looking up 'name' with the given mode, or
1732+
cell: The cell corresponding to looking up 'name' with the given scope, or
17281733
None if it's not found.
17291734
var_frame: The frame it should be set to or deleted from.
17301735
"""
17311736
if which_scopes == scope_e.Dynamic:
17321737
for i in xrange(len(self.var_stack) - 1, -1, -1):
17331738
var_frame = self.var_stack[i]
1734-
cell, result_frame = _FrameLookup(var_frame, name)
1739+
cell, result_frame = _FrameLookup(var_frame, name, bool(flags & YshDecl))
17351740
if cell:
17361741
return cell, result_frame
17371742
return None, self.var_stack[0] # set in global var_frame
17381743

17391744
if which_scopes == scope_e.LocalOnly:
1745+
# Stack:
1746+
# CommandEvaluator::_DoVarDecl
1747+
# Mem::SetNamed - do we need a flag for 'var'?
1748+
# Mem::_ResolveNameOrRef
1749+
# Mem::_ResolveNameOnly
1750+
#raise AssertionError()
1751+
17401752
var_frame = self.var_stack[-1]
1741-
cell, result_frame = _FrameLookup(var_frame, name)
1753+
cell, result_frame = _FrameLookup(var_frame, name, bool(flags & YshDecl))
17421754
if cell:
17431755
return cell, result_frame
17441756
return None, var_frame
17451757

17461758
if which_scopes == scope_e.GlobalOnly:
17471759
var_frame = self.var_stack[0]
1748-
cell, result_frame = _FrameLookup(var_frame, name)
1760+
cell, result_frame = _FrameLookup(var_frame, name, bool(flags & YshDecl))
17491761
if cell:
17501762
return cell, result_frame
17511763

@@ -1754,13 +1766,13 @@ def _ResolveNameOnly(self, name, which_scopes):
17541766
if which_scopes == scope_e.LocalOrGlobal:
17551767
# Local
17561768
var_frame = self.var_stack[-1]
1757-
cell, result_frame = _FrameLookup(var_frame, name)
1769+
cell, result_frame = _FrameLookup(var_frame, name, bool(flags & YshDecl))
17581770
if cell:
17591771
return cell, result_frame
17601772

17611773
# Global
17621774
var_frame = self.var_stack[0]
1763-
cell, result_frame = _FrameLookup(var_frame, name)
1775+
cell, result_frame = _FrameLookup(var_frame, name, bool(flags & YshDecl))
17641776
if cell:
17651777
return cell, result_frame
17661778

@@ -1772,14 +1784,15 @@ def _ResolveNameOrRef(
17721784
self,
17731785
name, # type: str
17741786
which_scopes, # type: scope_t
1787+
flags, # type: int
17751788
ref_trail=None, # type: Optional[List[str]]
17761789
):
17771790
# type: (...) -> Tuple[Optional[Cell], Dict[str, Cell], str]
1778-
"""Look up a cell and namespace, but respect the nameref flag.
1791+
"""Look up a cell and frame, but respect the nameref flag.
17791792
17801793
Resolving namerefs does RECURSIVE calls.
17811794
"""
1782-
cell, var_frame = self._ResolveNameOnly(name, which_scopes)
1795+
cell, var_frame = self._ResolveNameOnly(name, which_scopes, flags)
17831796

17841797
if cell is None or not cell.nameref:
17851798
return cell, var_frame, name # not a nameref
@@ -1827,7 +1840,7 @@ def _ResolveNameOrRef(
18271840

18281841
# 'declare -n' uses dynamic scope.
18291842
cell, var_frame, cell_name = self._ResolveNameOrRef(
1830-
new_name, scope_e.Dynamic, ref_trail=ref_trail)
1843+
new_name, scope_e.Dynamic, flags, ref_trail=ref_trail)
18311844
return cell, var_frame, cell_name
18321845

18331846
def IsBashAssoc(self, name):
@@ -1837,7 +1850,7 @@ def IsBashAssoc(self, name):
18371850
We need to know this to evaluate the index expression properly
18381851
-- should it be coerced to an integer or not?
18391852
"""
1840-
cell, _, _ = self._ResolveNameOrRef(name, self.ScopesForReading())
1853+
cell, _, _ = self._ResolveNameOrRef(name, self.ScopesForReading(), 0)
18411854
# foo=([key]=value)
18421855
return cell is not None and cell.val.tag() == value_e.BashAssoc
18431856

@@ -1890,6 +1903,8 @@ def SetLocalName(self, lval, val):
18901903

18911904
# Equivalent to
18921905
# self._ResolveNameOnly(lval.name, scope_e.LocalOnly)
1906+
1907+
# Note: doesn't use _FrameLookup
18931908
var_frame = self.var_stack[-1]
18941909
cell = var_frame.get(lval.name)
18951910

@@ -1906,7 +1921,7 @@ def SetNamed(self, lval, val, which_scopes, flags=0):
19061921
# type: (LeftName, value_t, scope_t, int) -> None
19071922
if flags & SetNameref or flags & ClearNameref:
19081923
# declare -n ref=x # refers to the ref itself
1909-
cell, var_frame = self._ResolveNameOnly(lval.name, which_scopes)
1924+
cell, var_frame = self._ResolveNameOnly(lval.name, which_scopes, flags)
19101925
cell_name = lval.name
19111926
else:
19121927
# ref=x # mutates THROUGH the reference
@@ -1918,7 +1933,7 @@ def SetNamed(self, lval, val, which_scopes, flags=0):
19181933
# 3. Turn BracedVarSub into an sh_lvalue, and call
19191934
# self.unsafe_arith.SetValue() wrapper with ref_trail
19201935
cell, var_frame, cell_name = self._ResolveNameOrRef(
1921-
lval.name, which_scopes)
1936+
lval.name, which_scopes, flags)
19221937

19231938
if cell:
19241939
# Clear before checking readonly bit.
@@ -2021,7 +2036,7 @@ def SetValue(self, lval, val, which_scopes, flags=0):
20212036
# Undef, which then turns into an INDEXED array. (Undef means that set
20222037
# -o nounset fails.)
20232038
cell, var_frame, _ = self._ResolveNameOrRef(
2024-
lval.name, which_scopes)
2039+
lval.name, which_scopes, flags)
20252040
if not cell:
20262041
self._BindNewArrayWithEntry(var_frame, lval, rval, flags,
20272042
left_loc)
@@ -2083,7 +2098,7 @@ def SetValue(self, lval, val, which_scopes, flags=0):
20832098
left_loc = lval.blame_loc
20842099

20852100
cell, var_frame, _ = self._ResolveNameOrRef(
2086-
lval.name, which_scopes)
2101+
lval.name, which_scopes, flags)
20872102
if cell.readonly:
20882103
e_die("Can't assign to readonly associative array",
20892104
left_loc)
@@ -2295,7 +2310,7 @@ def GetValue(self, name, which_scopes=scope_e.Shopt):
22952310
# 1. Call self.unsafe_arith.ParseVarRef() -> BracedVarSub
22962311
# 2. Call self.unsafe_arith.GetNameref(bvs_part), and get a value_t
22972312
# We still need a ref_trail to detect cycles.
2298-
cell, _, _ = self._ResolveNameOrRef(name, which_scopes)
2313+
cell, _, _ = self._ResolveNameOrRef(name, which_scopes, 0)
22992314
if cell:
23002315
return cell.val
23012316

@@ -2321,7 +2336,7 @@ def GetCell(self, name, which_scopes=scope_e.Shopt):
23212336
if which_scopes == scope_e.Shopt:
23222337
which_scopes = self.ScopesForReading()
23232338

2324-
cell, _ = self._ResolveNameOnly(name, which_scopes)
2339+
cell, _ = self._ResolveNameOnly(name, which_scopes, 0)
23252340
return cell
23262341

23272342
def GetCellDeref(self, name, which_scopes=scope_e.Shopt):
@@ -2332,7 +2347,7 @@ def GetCellDeref(self, name, which_scopes=scope_e.Shopt):
23322347
if which_scopes == scope_e.Shopt:
23332348
which_scopes = self.ScopesForReading()
23342349

2335-
cell, _, _ = self._ResolveNameOrRef(name, which_scopes)
2350+
cell, _, _ = self._ResolveNameOrRef(name, which_scopes, 0)
23362351
return cell
23372352

23382353
def Unset(self, lval, which_scopes):
@@ -2361,7 +2376,7 @@ def Unset(self, lval, which_scopes):
23612376
which_scopes = self.ScopesForWriting()
23622377

23632378
cell, var_frame, cell_name = self._ResolveNameOrRef(
2364-
var_name, which_scopes)
2379+
var_name, which_scopes, 0)
23652380
if not cell:
23662381
return False # 'unset' builtin falls back on functions
23672382
if cell.readonly:
@@ -2445,7 +2460,7 @@ def ClearFlag(self, name, flag):
24452460
We don't use SetValue() because even if rval is None, it will make an
24462461
Undef value in a scope.
24472462
"""
2448-
cell, var_frame = self._ResolveNameOnly(name, self.ScopesForReading())
2463+
cell, var_frame = self._ResolveNameOnly(name, self.ScopesForReading(), 0)
24492464
if cell:
24502465
if flag & ClearExport:
24512466
cell.exported = False

demo/survey-closure.rb

+26
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,29 @@ def create_context(x)
2121
# Execute the block in the context
2222
binding.instance_exec(&myproc)
2323

24+
25+
#
26+
# 2025-03 - var vs. setvar
27+
#
28+
29+
puts ""
30+
31+
def outer_function
32+
counter = 10 # Outer function defines its own `counter`
33+
puts "Counter in outer_function is: #{counter}"
34+
35+
# Inner function also defines its own `counter`
36+
inner_function = lambda do
37+
counter = 5 # Inner function defines its own `counter`
38+
puts "Mutating counter in inner_function: #{counter}"
39+
end
40+
41+
# Calling the inner function
42+
inner_function.call
43+
44+
# Back to outer function, `counter` remains unchanged
45+
puts "Counter in outer_function after inner call: #{counter}"
46+
end
47+
48+
outer_function
49+

osh/cmd_eval.py

+7-3
Original file line numberDiff line numberDiff line change
@@ -713,21 +713,25 @@ def _EvalCaseArg(self, arg, blame):
713713

714714
def _DoVarDecl(self, node):
715715
# type: (command.VarDecl) -> int
716-
# x = 'foo' in Hay blocks
716+
717+
flags = state.YshDecl
718+
719+
# x = 'foo' in Hay blocks is like 'const'
717720
if node.keyword is None:
718721
# Note: there's only one LHS
719722
lhs0 = node.lhs[0]
720723
lval = LeftName(lhs0.name, lhs0.left)
721724
assert node.rhs is not None, node
722725
val = self.expr_ev.EvalExpr(node.rhs, loc.Missing)
723726

727+
flags |= state.SetReadOnly
724728
self.mem.SetNamed(lval,
725729
val,
726730
scope_e.LocalOnly,
727-
flags=state.SetReadOnly)
731+
flags=flags)
728732

729733
else: # var or const
730-
flags = (state.SetReadOnly
734+
flags |= (state.SetReadOnly
731735
if node.keyword.id == Id.KW_Const else 0)
732736

733737
# var x, y does null initialization

spec/hay.test.sh

+51-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## oils_failures_allowed: 7
1+
## oils_failures_allowed: 4
22

33
# Hay: Hay Ain't YAML
44

@@ -486,9 +486,58 @@ Package {
486486
}
487487
}
488488

489-
pp test_ (_hay())
489+
json write (_hay())
490490

491491
## STDOUT:
492+
{
493+
"source": null,
494+
"children": [
495+
{
496+
"type": "Package",
497+
"args": [],
498+
"children": [
499+
{
500+
"type": "Deps",
501+
"args": [],
502+
"children": [],
503+
"attrs": {
504+
"x": 20
505+
}
506+
}
507+
],
508+
"attrs": {
509+
"x": 10
510+
}
511+
}
512+
]
513+
}
514+
## END
515+
516+
#### Param with same name as Hay attribute
517+
shopt --set ysh:all
518+
519+
# Danilo reported this on Zulip
520+
521+
hay define Service
522+
523+
proc gen-service(; ; variant = null) {
524+
Service {
525+
variant = variant
526+
port = 80
527+
}
528+
}
529+
530+
gen-service
531+
gen-service (variant = 'z')
532+
533+
var attrs = _hay().children[0].attrs
534+
json write (attrs)
535+
536+
## STDOUT:
537+
{
538+
"variant": null,
539+
"port": 80
540+
}
492541
## END
493542

494543

spec/ysh-builtin-eval.test.sh

+13-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# YSH specific features of eval
22

33
## our_shell: ysh
4-
## oils_failures_allowed: 2
4+
## oils_failures_allowed: 1
55

66
#### eval builtin does not take a literal block - can restore this later
77

@@ -385,6 +385,16 @@ Dict (&d) {
385385

386386
pp test_ (d)
387387

388+
389+
# Same problem as Hay test cases!
390+
const c = 99
391+
392+
Dict (&d2) {
393+
const c = 101
394+
}
395+
396+
pp test_ (d2)
397+
388398
exit
389399

390400
# restored to the shadowed values
@@ -402,6 +412,8 @@ proc p {
402412
}
403413

404414
## STDOUT:
415+
(Dict) {"bare":42,"k":"k-block-mutated","k3":"k3"}
416+
(Dict) {"c":101}
405417
## END
406418

407419
#### block in Dict (&d) { ... } can read from outer scope

0 commit comments

Comments
 (0)