Skip to content

Commit

Permalink
Merge pull request #10 from SeeminglyScience/fix-empty-hashtable
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
SeeminglyScience authored Apr 24, 2018
2 parents 6346419 + 48500e1 commit d22aaac
Show file tree
Hide file tree
Showing 6 changed files with 405 additions and 14 deletions.
32 changes: 29 additions & 3 deletions src/PSLambda/CompileVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -244,9 +244,9 @@ public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst)
case TokenKind.Or:
return OrElse(PSIsTrue(lhs), PSIsTrue(rhs));
case TokenKind.Band:
return And(lhs, rhs);
return PSBitwiseOperation(ExpressionType.And, lhs, rhs);
case TokenKind.Bor:
return Or(lhs, rhs);
return PSBitwiseOperation(ExpressionType.Or, lhs, rhs);
case TokenKind.Is:
if (rhsTypeConstant == null)
{
Expand Down Expand Up @@ -488,6 +488,14 @@ public object VisitForStatement(ForStatementAst forStatementAst)

public object VisitHashtable(HashtableAst hashtableAst)
{
if (hashtableAst.KeyValuePairs.Count == 0)
{
return New(
ReflectionCache.Hashtable_Ctor,
Constant(0),
Property(null, ReflectionCache.StringComparer_CurrentCultureIgnoreCase));
}

var elements = new ElementInit[hashtableAst.KeyValuePairs.Count];
for (var i = 0; i < elements.Length; i++)
{
Expand Down Expand Up @@ -536,8 +544,15 @@ public object VisitIndexExpression(IndexExpressionAst indexExpressionAst)
new[] { indexExpressionAst.Index.Compile(this) });
}

if (TryFindGenericInterface(source.Type, typeof(IEnumerable<>), out Type genericEnumerable))
if (TryFindGenericInterface(source.Type, typeof(IEnumerable<>), out Type genericEnumerable) ||
(source.Type.IsGenericType &&
source.Type.GetGenericTypeDefinition() == typeof(IEnumerable<>)))
{
if (genericEnumerable == null)
{
genericEnumerable = source.Type;
}

Expression index;
try
{
Expand Down Expand Up @@ -787,6 +802,15 @@ public object VisitSwitchStatement(SwitchStatementAst switchStatementAst)
{
using (_loops.NewScope())
{
if (switchStatementAst.Clauses.Count == 0)
{
return new[]
{
switchStatementAst.Default.Compile(this),
Label(_loops.Break)
};
}

var clauses = new SwitchCase[switchStatementAst.Clauses.Count];
for (var i = 0; i < clauses.Length; i++)
{
Expand Down Expand Up @@ -892,6 +916,8 @@ public object VisitUnaryExpression(UnaryExpressionAst unaryExpressionAst)
return Assign(child, Increment(child));
case TokenKind.PostfixMinusMinus:
return Assign(child, Decrement(child));
case TokenKind.Not:
return Not(PSIsTrue(child));
default:
ReportNotSupported(
unaryExpressionAst.Extent,
Expand Down
33 changes: 33 additions & 0 deletions src/PSLambda/ExpressionUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,39 @@ public static Expression PSDotDot(Expression lhs, Expression rhs)
PSConvertTo<int>(rhs));
}

/// <summary>
/// Creates an <see cref="Expression" /> representing the evaluation of a bitwise comparision
/// operator from the PowerShell engine.
/// </summary>
/// <param name="expressionType">The expression operator.</param>
/// <param name="lhs">The <see cref="Expression" /> on the left hand side.</param>
/// <param name="rhs">The <see cref="Expression" /> on the right hand side.</param>
/// <returns>An <see cref="Expression" /> representing the operation.</returns>
public static Expression PSBitwiseOperation(
ExpressionType expressionType,
Expression lhs,
Expression rhs)
{
var resultType = lhs.Type;
if (typeof(Enum).IsAssignableFrom(lhs.Type))
{
lhs = Convert(lhs, Enum.GetUnderlyingType(lhs.Type));
}

if (typeof(Enum).IsAssignableFrom(rhs.Type))
{
rhs = Convert(rhs, Enum.GetUnderlyingType(rhs.Type));
}

var resultExpression = MakeBinary(expressionType, lhs, rhs);
if (resultType == resultExpression.Type)
{
return resultExpression;
}

return PSConvertTo(resultExpression, resultType);
}

private static bool PSEqualsIgnoreCase(object first, object second)
{
return LanguagePrimitives.Compare(first, second, ignoreCase: true) == 0;
Expand Down
2 changes: 1 addition & 1 deletion src/PSLambda/ReflectionCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ internal static class ReflectionCache
var parameters = method.GetParameters();
return parameters.Length == 2
&& parameters[0].ParameterType == typeof(object)
&& parameters[1].ParameterType.IsGenericParameter;
&& parameters[1].ParameterType.IsByRef;
},
null)
.FirstOrDefault();
Expand Down
81 changes: 81 additions & 0 deletions test/Loops.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,85 @@ Describe 'basic loop functionality' {

$delegate.Invoke() | Should -Be 11
}

It 'break continues after loop' {
$delegate = New-PSDelegate {
$i = 0
$continuedAfterBreak = $false
while ($i -lt 10) {
$i++
if ($i -eq 5) {
break
$continuedAfterBreak = $true
}
}

if ($continuedAfterBreak) {
throw 'code after "break" was executed'
}

return $i
}

$delegate.Invoke() | Should -Be 5
}

It 'continue steps to next interation' {
$delegate = New-PSDelegate {
$i = 0
$continuedAfterContinue = $false
while ($i -lt 10) {
$i++
continue
$continuedAfterContinue = $true
}

if ($continuedAfterContinue) {
throw 'code after "continue" was executed'
}

return $i
}

$delegate.Invoke() | Should -Be 10
}

Context 'switch statement' {
It 'chooses correct value' {
$hitValues = [System.Collections.Generic.List[string]]::new()
$delegate = New-PSDelegate {
foreach ($value in 'value1', 'value2', 'value3', 'invalid') {
switch ($value) {
value1 { $hitValues.Add('option1') }
value2 { $hitValues.Add('option2') }
value3 { $hitValues.Add('option3') }
default { throw }
}
}
}

{ $delegate.Invoke() } | Should -Throw
$hitValues | Should -Be 'option1', 'option2', 'option3'
}

It 'can have a single value' {
$delegate = New-PSDelegate {
switch ('value') {
value { return 'value' }
}
}

$delegate.Invoke() | Should -Be 'value'
}

It 'can have a default without cases' {
$delegate = New-PSDelegate {
switch ('value') {
default { return 'value' }
}
}

$delegate.Invoke() | Should -Be 'value'
}
}
}
163 changes: 153 additions & 10 deletions test/MiscLanguageFeatures.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,37 @@ $manifestPath = "$PSScriptRoot\..\Release\$moduleName\*\$moduleName.psd1"
Import-Module $manifestPath -Force

Describe 'Misc Language Features' {
It 'Hashtable expression' {
It 'expandable string expression' {
$delegate = New-PSDelegate {
return @{
'string' = 'value'
string2 = 10
Object = [object]::new()
$one = "1"
$two = 2
$three = 'something'
return "This is a string with $one random $two numbers and $three"
}

$delegate.Invoke() | Should -Be 'This is a string with 1 random 2 numbers and something'
}

Context 'hashtable tests' {
It 'handles varied types of values' {
$delegate = New-PSDelegate {
return @{
'string' = 'value'
string2 = 10
Object = [object]::new()
}
}

$hashtable = $delegate.Invoke()
$hashtable['string'] | Should -Be 'value'
$hashtable['string2'] | Should -Be 10
$hashtable['Object'] | Should -BeOfType object
$hashtable['object'] | Should -BeOfType object
}

$hashtable = $delegate.Invoke()
$hashtable['string'] | Should -Be 'value'
$hashtable['string2'] | Should -Be 10
$hashtable['Object'] | Should -BeOfType object
$hashtable['object'] | Should -BeOfType object
It 'can initialize an empty hashtable' {
(New-PSDelegate { @{} }).Invoke().GetType() | Should -Be ([hashtable])
}
}

Context 'array literal' {
Expand Down Expand Up @@ -60,6 +77,26 @@ Describe 'Misc Language Features' {
$result[0].GetType() | Should -Be ([int])
$result | Should -Be 1
}

It 'creates an empty array' {
$result = (New-PSDelegate { @() }).Invoke()
$result.GetType() | Should -Be ([object[]])
$result.Length | Should -Be 0
}

It 'can take multiple statements in an array' {
$delegate = New-PSDelegate {
return @(
0..10
10..20)
}

$result = $delegate.Invoke()
$result.GetType() | Should -Be ([int[][]])
$result.Count | Should -Be 2
$result[0] | Should -Be (0..10)
$result[1] | Should -Be (10..20)
}
}

Context 'assignments' {
Expand All @@ -86,5 +123,111 @@ Describe 'Misc Language Features' {
{ (New-PSDelegate { if ($true) { $a = 10 }; return $a }).Invoke() } |
Should -Throw 'The variable "a" was referenced before it was defined or was defined in a sibling scope'
}

It 'can assign to an index operation' {
$delegate = New-PSDelegate {
$hash = @{ Key = 'Value' }
$hash['Key'] = 'NewValue'
return $hash
}

$delegate.Invoke().Key | Should -Be 'NewValue'
}

It 'can assign to a property' {
$delegate = New-PSDelegate {
$verboseRecord = [System.Management.Automation.VerboseRecord]::new('original message')
$verboseRecord.Message = 'new message'
return $verboseRecord
}

$delegate.Invoke().Message | Should -Be 'new message'
}

It 'minus equals' {
$delegate = New-PSDelegate {
$a = 10
$a -= 5
return $a
}

$delegate.Invoke() | Should -Be 5
}

It 'multiply equals' {
$delegate = New-PSDelegate {
$a = 10
$a *= 5
return $a
}

$delegate.Invoke() | Should -Be 50
}

It 'divide equals' {
$delegate = New-PSDelegate {
$a = 10
$a /= 5
return $a
}

$delegate.Invoke() | Should -Be 2
}

It 'remainder equals' {
$delegate = New-PSDelegate {
$a = 10
$a %= 6
return $a
}

$delegate.Invoke() | Should -Be 4
}
}

Context 'indexer inference' {
It 'indexed IList<> are typed property' {
$delegate = New-PSDelegate {
$list = [System.Collections.Generic.List[string]]::new()
$list.Add('test')
return $list[0].EndsWith('t')
}

$delegate.Invoke() | Should -Be $true
}

It 'indexed IDictionary<,> are typed property' {
$delegate = New-PSDelegate {
$list = [System.Collections.Generic.Dictionary[string, type]]::new()
$list.Add('test', [type])
return $list['test'].Namespace
}

$delegate.Invoke() | Should -Be 'System'
}

It 'indexed IEnumerable<> are typed properly' {
$delegate = New-PSDelegate {
$strings = generic(
[System.Linq.Enumerable]::Select(
('test', 'test2', 'test3'),
[func[string, string]]{ ($string) => { $string }}),
[string], [string])

return $strings[1].EndsWith('2')
}

$delegate.Invoke() | Should -Be $true
}

It 'can index IList' {
$delegate = New-PSDelegate {
$list = [System.Collections.ArrayList]::new()
$list.AddRange([object[]]('one', 'two', 'three'))
return [string]$list[1] -eq 'two'
}

$delegate.Invoke() | Should -Be $true
}
}
}
Loading

0 comments on commit d22aaac

Please sign in to comment.