Skip to content

Commit d22aaac

Browse files
Merge pull request #10 from SeeminglyScience/fix-empty-hashtable
Fix empty hashtables, -as, -not, -bor, -band, and IEnumerable<> index expressions If a hashtable expression does not contain elements it now generate a `New` expression instead of a `ListInit` expression. The operators `-band` and `-bor` required conversion for `Enum` types. The comparison is now performed on the underlying type and then converted to the type of the lhs expression if applicable. If an expression was typed as IEnumerable<> specifically (like Linq method results) the indexer was not being inferred correctly. If a switch statement only has a "default" case it will be replaced with an expression that is just the default body and a break label. Forgot to put in support for `-not`, and `-as` was throwing a NRE because of a method resolution error on my part.
2 parents 6346419 + 48500e1 commit d22aaac

File tree

6 files changed

+405
-14
lines changed

6 files changed

+405
-14
lines changed

src/PSLambda/CompileVisitor.cs

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,9 @@ public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst)
244244
case TokenKind.Or:
245245
return OrElse(PSIsTrue(lhs), PSIsTrue(rhs));
246246
case TokenKind.Band:
247-
return And(lhs, rhs);
247+
return PSBitwiseOperation(ExpressionType.And, lhs, rhs);
248248
case TokenKind.Bor:
249-
return Or(lhs, rhs);
249+
return PSBitwiseOperation(ExpressionType.Or, lhs, rhs);
250250
case TokenKind.Is:
251251
if (rhsTypeConstant == null)
252252
{
@@ -488,6 +488,14 @@ public object VisitForStatement(ForStatementAst forStatementAst)
488488

489489
public object VisitHashtable(HashtableAst hashtableAst)
490490
{
491+
if (hashtableAst.KeyValuePairs.Count == 0)
492+
{
493+
return New(
494+
ReflectionCache.Hashtable_Ctor,
495+
Constant(0),
496+
Property(null, ReflectionCache.StringComparer_CurrentCultureIgnoreCase));
497+
}
498+
491499
var elements = new ElementInit[hashtableAst.KeyValuePairs.Count];
492500
for (var i = 0; i < elements.Length; i++)
493501
{
@@ -536,8 +544,15 @@ public object VisitIndexExpression(IndexExpressionAst indexExpressionAst)
536544
new[] { indexExpressionAst.Index.Compile(this) });
537545
}
538546

539-
if (TryFindGenericInterface(source.Type, typeof(IEnumerable<>), out Type genericEnumerable))
547+
if (TryFindGenericInterface(source.Type, typeof(IEnumerable<>), out Type genericEnumerable) ||
548+
(source.Type.IsGenericType &&
549+
source.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
540550
{
551+
if (genericEnumerable == null)
552+
{
553+
genericEnumerable = source.Type;
554+
}
555+
541556
Expression index;
542557
try
543558
{
@@ -787,6 +802,15 @@ public object VisitSwitchStatement(SwitchStatementAst switchStatementAst)
787802
{
788803
using (_loops.NewScope())
789804
{
805+
if (switchStatementAst.Clauses.Count == 0)
806+
{
807+
return new[]
808+
{
809+
switchStatementAst.Default.Compile(this),
810+
Label(_loops.Break)
811+
};
812+
}
813+
790814
var clauses = new SwitchCase[switchStatementAst.Clauses.Count];
791815
for (var i = 0; i < clauses.Length; i++)
792816
{
@@ -892,6 +916,8 @@ public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst)
892916
return Assign(child, Increment(child));
893917
case TokenKind.PostfixMinusMinus:
894918
return Assign(child, Decrement(child));
919+
case TokenKind.Not:
920+
return Not(PSIsTrue(child));
895921
default:
896922
ReportNotSupported(
897923
unaryExpressionAst.Extent,

src/PSLambda/ExpressionUtils.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,6 +352,39 @@ public static Expression PSDotDot(Expression lhs, Expression rhs)
352352
PSConvertTo<int>(rhs));
353353
}
354354

355+
/// <summary>
356+
/// Creates an <see cref="Expression" /> representing the evaluation of a bitwise comparision
357+
/// operator from the PowerShell engine.
358+
/// </summary>
359+
/// <param name="expressionType">The expression operator.</param>
360+
/// <param name="lhs">The <see cref="Expression" /> on the left hand side.</param>
361+
/// <param name="rhs">The <see cref="Expression" /> on the right hand side.</param>
362+
/// <returns>An <see cref="Expression" /> representing the operation.</returns>
363+
public static Expression PSBitwiseOperation(
364+
ExpressionType expressionType,
365+
Expression lhs,
366+
Expression rhs)
367+
{
368+
var resultType = lhs.Type;
369+
if (typeof(Enum).IsAssignableFrom(lhs.Type))
370+
{
371+
lhs = Convert(lhs, Enum.GetUnderlyingType(lhs.Type));
372+
}
373+
374+
if (typeof(Enum).IsAssignableFrom(rhs.Type))
375+
{
376+
rhs = Convert(rhs, Enum.GetUnderlyingType(rhs.Type));
377+
}
378+
379+
var resultExpression = MakeBinary(expressionType, lhs, rhs);
380+
if (resultType == resultExpression.Type)
381+
{
382+
return resultExpression;
383+
}
384+
385+
return PSConvertTo(resultExpression, resultType);
386+
}
387+
355388
private static bool PSEqualsIgnoreCase(object first, object second)
356389
{
357390
return LanguagePrimitives.Compare(first, second, ignoreCase: true) == 0;

src/PSLambda/ReflectionCache.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ internal static class ReflectionCache
6161
var parameters = method.GetParameters();
6262
return parameters.Length == 2
6363
&& parameters[0].ParameterType == typeof(object)
64-
&& parameters[1].ParameterType.IsGenericParameter;
64+
&& parameters[1].ParameterType.IsByRef;
6565
},
6666
null)
6767
.FirstOrDefault();

test/Loops.Tests.ps1

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,85 @@ Describe 'basic loop functionality' {
7070

7171
$delegate.Invoke() | Should -Be 11
7272
}
73+
74+
It 'break continues after loop' {
75+
$delegate = New-PSDelegate {
76+
$i = 0
77+
$continuedAfterBreak = $false
78+
while ($i -lt 10) {
79+
$i++
80+
if ($i -eq 5) {
81+
break
82+
$continuedAfterBreak = $true
83+
}
84+
}
85+
86+
if ($continuedAfterBreak) {
87+
throw 'code after "break" was executed'
88+
}
89+
90+
return $i
91+
}
92+
93+
$delegate.Invoke() | Should -Be 5
94+
}
95+
96+
It 'continue steps to next interation' {
97+
$delegate = New-PSDelegate {
98+
$i = 0
99+
$continuedAfterContinue = $false
100+
while ($i -lt 10) {
101+
$i++
102+
continue
103+
$continuedAfterContinue = $true
104+
}
105+
106+
if ($continuedAfterContinue) {
107+
throw 'code after "continue" was executed'
108+
}
109+
110+
return $i
111+
}
112+
113+
$delegate.Invoke() | Should -Be 10
114+
}
115+
116+
Context 'switch statement' {
117+
It 'chooses correct value' {
118+
$hitValues = [System.Collections.Generic.List[string]]::new()
119+
$delegate = New-PSDelegate {
120+
foreach ($value in 'value1', 'value2', 'value3', 'invalid') {
121+
switch ($value) {
122+
value1 { $hitValues.Add('option1') }
123+
value2 { $hitValues.Add('option2') }
124+
value3 { $hitValues.Add('option3') }
125+
default { throw }
126+
}
127+
}
128+
}
129+
130+
{ $delegate.Invoke() } | Should -Throw
131+
$hitValues | Should -Be 'option1', 'option2', 'option3'
132+
}
133+
134+
It 'can have a single value' {
135+
$delegate = New-PSDelegate {
136+
switch ('value') {
137+
value { return 'value' }
138+
}
139+
}
140+
141+
$delegate.Invoke() | Should -Be 'value'
142+
}
143+
144+
It 'can have a default without cases' {
145+
$delegate = New-PSDelegate {
146+
switch ('value') {
147+
default { return 'value' }
148+
}
149+
}
150+
151+
$delegate.Invoke() | Should -Be 'value'
152+
}
153+
}
73154
}

test/MiscLanguageFeatures.Tests.ps1

Lines changed: 153 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,37 @@ $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
44
Import-Module $manifestPath -Force
55

66
Describe 'Misc Language Features' {
7-
It 'Hashtable expression' {
7+
It 'expandable string expression' {
88
$delegate = New-PSDelegate {
9-
return @{
10-
'string' = 'value'
11-
string2 = 10
12-
Object = [object]::new()
9+
$one = "1"
10+
$two = 2
11+
$three = 'something'
12+
return "This is a string with $one random $two numbers and $three"
13+
}
14+
15+
$delegate.Invoke() | Should -Be 'This is a string with 1 random 2 numbers and something'
16+
}
17+
18+
Context 'hashtable tests' {
19+
It 'handles varied types of values' {
20+
$delegate = New-PSDelegate {
21+
return @{
22+
'string' = 'value'
23+
string2 = 10
24+
Object = [object]::new()
25+
}
1326
}
27+
28+
$hashtable = $delegate.Invoke()
29+
$hashtable['string'] | Should -Be 'value'
30+
$hashtable['string2'] | Should -Be 10
31+
$hashtable['Object'] | Should -BeOfType object
32+
$hashtable['object'] | Should -BeOfType object
1433
}
1534

16-
$hashtable = $delegate.Invoke()
17-
$hashtable['string'] | Should -Be 'value'
18-
$hashtable['string2'] | Should -Be 10
19-
$hashtable['Object'] | Should -BeOfType object
20-
$hashtable['object'] | Should -BeOfType object
35+
It 'can initialize an empty hashtable' {
36+
(New-PSDelegate { @{} }).Invoke().GetType() | Should -Be ([hashtable])
37+
}
2138
}
2239

2340
Context 'array literal' {
@@ -60,6 +77,26 @@ Describe 'Misc Language Features' {
6077
$result[0].GetType() | Should -Be ([int])
6178
$result | Should -Be 1
6279
}
80+
81+
It 'creates an empty array' {
82+
$result = (New-PSDelegate { @() }).Invoke()
83+
$result.GetType() | Should -Be ([object[]])
84+
$result.Length | Should -Be 0
85+
}
86+
87+
It 'can take multiple statements in an array' {
88+
$delegate = New-PSDelegate {
89+
return @(
90+
0..10
91+
10..20)
92+
}
93+
94+
$result = $delegate.Invoke()
95+
$result.GetType() | Should -Be ([int[][]])
96+
$result.Count | Should -Be 2
97+
$result[0] | Should -Be (0..10)
98+
$result[1] | Should -Be (10..20)
99+
}
63100
}
64101

65102
Context 'assignments' {
@@ -86,5 +123,111 @@ Describe 'Misc Language Features' {
86123
{ (New-PSDelegate { if ($true) { $a = 10 }; return $a }).Invoke() } |
87124
Should -Throw 'The variable "a" was referenced before it was defined or was defined in a sibling scope'
88125
}
126+
127+
It 'can assign to an index operation' {
128+
$delegate = New-PSDelegate {
129+
$hash = @{ Key = 'Value' }
130+
$hash['Key'] = 'NewValue'
131+
return $hash
132+
}
133+
134+
$delegate.Invoke().Key | Should -Be 'NewValue'
135+
}
136+
137+
It 'can assign to a property' {
138+
$delegate = New-PSDelegate {
139+
$verboseRecord = [System.Management.Automation.VerboseRecord]::new('original message')
140+
$verboseRecord.Message = 'new message'
141+
return $verboseRecord
142+
}
143+
144+
$delegate.Invoke().Message | Should -Be 'new message'
145+
}
146+
147+
It 'minus equals' {
148+
$delegate = New-PSDelegate {
149+
$a = 10
150+
$a -= 5
151+
return $a
152+
}
153+
154+
$delegate.Invoke() | Should -Be 5
155+
}
156+
157+
It 'multiply equals' {
158+
$delegate = New-PSDelegate {
159+
$a = 10
160+
$a *= 5
161+
return $a
162+
}
163+
164+
$delegate.Invoke() | Should -Be 50
165+
}
166+
167+
It 'divide equals' {
168+
$delegate = New-PSDelegate {
169+
$a = 10
170+
$a /= 5
171+
return $a
172+
}
173+
174+
$delegate.Invoke() | Should -Be 2
175+
}
176+
177+
It 'remainder equals' {
178+
$delegate = New-PSDelegate {
179+
$a = 10
180+
$a %= 6
181+
return $a
182+
}
183+
184+
$delegate.Invoke() | Should -Be 4
185+
}
186+
}
187+
188+
Context 'indexer inference' {
189+
It 'indexed IList<> are typed property' {
190+
$delegate = New-PSDelegate {
191+
$list = [System.Collections.Generic.List[string]]::new()
192+
$list.Add('test')
193+
return $list[0].EndsWith('t')
194+
}
195+
196+
$delegate.Invoke() | Should -Be $true
197+
}
198+
199+
It 'indexed IDictionary<,> are typed property' {
200+
$delegate = New-PSDelegate {
201+
$list = [System.Collections.Generic.Dictionary[string, type]]::new()
202+
$list.Add('test', [type])
203+
return $list['test'].Namespace
204+
}
205+
206+
$delegate.Invoke() | Should -Be 'System'
207+
}
208+
209+
It 'indexed IEnumerable<> are typed properly' {
210+
$delegate = New-PSDelegate {
211+
$strings = generic(
212+
[System.Linq.Enumerable]::Select(
213+
('test', 'test2', 'test3'),
214+
[func[string, string]]{ ($string) => { $string }}),
215+
[string], [string])
216+
217+
return $strings[1].EndsWith('2')
218+
}
219+
220+
$delegate.Invoke() | Should -Be $true
221+
}
222+
223+
It 'can index IList' {
224+
$delegate = New-PSDelegate {
225+
$list = [System.Collections.ArrayList]::new()
226+
$list.AddRange([object[]]('one', 'two', 'three'))
227+
return [string]$list[1] -eq 'two'
228+
}
229+
230+
$delegate.Invoke() | Should -Be $true
231+
}
89232
}
90233
}

0 commit comments

Comments
 (0)