Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 157 additions & 1 deletion src/System.Management.Automation/engine/lang/parserutils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,92 @@ internal static object JoinOperator(ExecutionContext context, IScriptExtent erro
}
}

/// <summary>
/// The implementation of the PowerShell range operator.
/// </summary>
/// <param name="lval">The object on which to start.</param>
/// <param name="rval">The object on which to stop.</param>
/// <returns>The array of objects.</returns>
internal static object RangeOperator(object lval, object rval)
{
var lbase = PSObject.Base(lval);
var rbase = PSObject.Base(rval);

// If both arguments is [char] type or [string] type with length==1
// return objects of [char] type.
// In special case "0".."9" return objects of [int] type.
if (AsChar(lbase) is char lc && AsChar(rbase) is char rc)
{
return CharOps.Range(lc, rc);
}

// As a last resort, the range operator tries to return objects of [int] type.
// 1..10
// "1".."10"
// [int]"1"..[int]"10"
var l = Convert.ToInt32(lbase);
var r = Convert.ToInt32(rbase);

return IntOps.Range(l, r);
}

/// <summary>
/// The implementation of an enumerator for the PowerShell range operator.
/// </summary>
/// <param name="lval">The object on which to start.</param>
/// <param name="rval">The object on which to stop.</param>
/// <returns>The enumerator.</returns>
internal static IEnumerator GetRangeEnumerator(object lval, object rval)
{
var lbase = PSObject.Base(lval);
var rbase = PSObject.Base(rval);

// If both arguments is [char] type or [string] type with length==1
// return objects of [char] type.
// In special case "0".."9" return objects of [int] type.
if (AsChar(lbase) is char lc && AsChar(rbase) is char rc)
{
return new CharRangeEnumerator(lc, rc);
}

// As a last resort, the range operator tries to return objects of [int] type.
// 1..10
// "1".."10"
// [int]"1"..[int]"10"
var l = Convert.ToInt32(lbase);
var r = Convert.ToInt32(rbase);

return new RangeEnumerator(l, r);
}

// Help function for Range operator.
//
// In common case:
// for [char] type
// for [string] type and length == 1
// return objects of [char] type:
// [char]'A'..[char]'Z'
// [char]'A'..'Z'
// [char]'A'.."Z"
// 'A'..[char]'Z'
// "A"..[char]'Z'
// "A".."Z"
// [char]"A"..[string]"Z"
// "A"..[char]"Z"
// [string]"A".."Z"
// and so on.
//
// In special case:
// "0".."9"
// return objects of [int] type.
private static object AsChar(object obj)
{
if (obj is char) return obj;
if (obj is string str && str.Length == 1 && !Char.IsDigit(str, 0)) return str[0];
return null;
}


/// <summary>
/// The implementation of the PowerShell -replace operator....
/// </summary>
Expand Down Expand Up @@ -1478,7 +1564,13 @@ internal int UpperBound
}

private int _current;
public object Current

Object IEnumerator.Current
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should swap places with private int _current; so that the field can be closed to the property it backs.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you explain why this method was added? It seems like we only need the explicit method impl or the other, but not both.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I saw it in CoreFX implementations ( sample ). I don't remember exactly - it seems I'm checking that it doesn't work at all without this method if we use enheritance and call by IEnumerator interface.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@lzybkr What is your conclusion?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example you point to is needed to implement the strongly typed interface IEnumerable<T>. If you wanted to follow that pattern, that's fine, but you would replace the CurrentValue property with the Current property.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed.

{
get { return Current; }
}

public virtual int Current
{
get { return _current; }
}
Expand Down Expand Up @@ -1521,6 +1613,70 @@ public bool MoveNext()
return true;
}
}

/// <summary>
/// The simple enumerator class is used for the range operator '..'
/// in expressions like 'A'..'B' | ForEach-Object { $_ }
/// </summary>
internal class CharRangeEnumerator : IEnumerator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious if we should start following the one-type-per-file rule https://github.com/DotNetAnalyzers/StyleCopAnalyzers/blob/master/documentation/SA1402.md

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is often useful to have related types in the same file, so I'm strongly opposed to following this rule. I'm strongly in favor of organizing code in ways that make sense to the author and reviewers. We shouldn't blindly follow the guidelines of some tool.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree that it make sense to group related code and that following the rules blindly is silly. But, some of the files in this project are so massive that there is 0 benefit gained by the code being grouped, IMO. If I need to have 3 tabs open on the same file to work on it effectively, the code is probably better split, IMO. Doesn't have to be strictly followed, but, maybe give some serious consideration?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting the author of said code saw 0 benefit of a huge file, because you might be saying that to said author. (I'm the primary author of 4 of the 10 largest non-generated files in the repo.)

To pick on the largest file - Ast.cs - there are at least 72 classes in that file. When I know I'm looking for something Ast related, but not sure what, if I end up with 72 open tabs that I need to close, I'm less productive than if I open 1 file.

And if you needed 3 tabs open to effectively work on some code, you needed 3 tabs to see some code, 1 file or 3 files, right? I do realize

There is always room for improvement, but it should be thoughtful and not rely on some tool - tools are good at enforcing things that are easy to check, less good at providing good judgement.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you suggesting the author of said code saw 0 benefit of a huge file,

Not at all. I'm suggesting I see 0 benefit.

because you might be saying that to said author.

I know 🙂 .

And if you needed 3 tabs open to effectively work on some code, you needed 3 tabs to see some code, 1 file or 3 files, right?

Right, I'm saying I see no benefit to these being in the same file, where I do see benefit in it being in separate files. I think it's a difference of preference, I prefer smaller files grouped in folders over monolith files. It's easier to jump between code in different files using the IDE, IMO. When the code is in the same file, the default action is to jump to the code in the current tab. When it is in another file, a new tab opens with that file and my current place in the previous file is preserved. *shrugs

I'm also trying to approach this from a maintainable open source project perspective. If this rule doen't makes sense for this project, then it doesn't make sense and we don't use it. I just want to have something in place that can help maintain and encourage an enforceable style for the life of the project, preferably through CI. It can help shift the burden from reviewers to contributors.

If I'm wasting my efforts there, then I will gladly redirect my attention elsewhere.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this implement IEnumerable<String> instead?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we cast all to object type before send to pipe. So it seems there is no sense to specify here the exact type.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The code is basically the same as RangeEnumerator, I wonder if there is a clever way we could avoid the duplication.

One thought - you could maybe derive from RangeEnumerator and just override Current.

It's probably not worth spending time on this though - C# doesn't handle this scenario nicely like C++ template would.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started with this but it looked too confusing. In addition, I thought that if we wanted to enhance this operator in the future, it would be easier if each type would have its own enumerator. Also possible we can make optimization and reduce string allocations.

{
private int _increment = 1;

private bool _firstElement = true;

public CharRangeEnumerator(char lowerBound, char upperBound)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should probably go with the other methods in the class.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah I missed that this was the constructor... My bad

{
LowerBound = lowerBound;
Current = lowerBound;
UpperBound = upperBound;
if (lowerBound > upperBound)
_increment = -1;
}

Object IEnumerator.Current
{
get { return Current; }
}

internal char LowerBound
{
get; private set;
}

internal char UpperBound
{
get; private set;
}

public char Current
{
get; private set;
}

public bool MoveNext()
{
if (_firstElement)
{
_firstElement = false;
return true;
}

if (Current == UpperBound)
{
return false;
}

Current = (char)(Current + _increment);
return true;
}

public void Reset()
{
Current = LowerBound;
_firstElement = true;
}
}

#endregion RangeEnumerator

#region InterpreterError
Expand Down
38 changes: 16 additions & 22 deletions src/System.Management.Automation/engine/parser/Compiler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,6 @@ internal static class CachedReflectionInfo
typeof(CharOps).GetMethod(nameof(CharOps.CompareStringIeq), staticFlags);
internal static readonly MethodInfo CharOps_CompareStringIne =
typeof(CharOps).GetMethod(nameof(CharOps.CompareStringIne), staticFlags);
internal static readonly MethodInfo CharOps_Range =
typeof(CharOps).GetMethod(nameof(CharOps.Range), staticFlags);

internal static readonly MethodInfo CommandParameterInternal_CreateArgument =
typeof(CommandParameterInternal).GetMethod(nameof(CommandParameterInternal.CreateArgument), staticFlags);
internal static readonly MethodInfo CommandParameterInternal_CreateParameter =
Expand Down Expand Up @@ -248,9 +245,6 @@ internal static class CachedReflectionInfo
internal static readonly MethodInfo InterpreterError_NewInterpreterExceptionWithInnerException =
typeof(InterpreterError).GetMethod(nameof(InterpreterError.NewInterpreterExceptionWithInnerException), staticFlags);

internal static readonly MethodInfo IntOps_Range =
typeof(IntOps).GetMethod(nameof(IntOps.Range), staticFlags);

internal static readonly MethodInfo LanguagePrimitives_GetInvalidCastMessages =
typeof(LanguagePrimitives).GetMethod(nameof(LanguagePrimitives.GetInvalidCastMessages), staticFlags);
internal static readonly MethodInfo LanguagePrimitives_ThrowInvalidCastException =
Expand Down Expand Up @@ -292,6 +286,10 @@ internal static class CachedReflectionInfo
typeof(ParserOps).GetMethod(nameof(ParserOps.LikeOperator), staticFlags);
internal static readonly MethodInfo ParserOps_MatchOperator =
typeof(ParserOps).GetMethod(nameof(ParserOps.MatchOperator), staticFlags);
internal static readonly MethodInfo ParserOps_RangeOperator =
typeof(ParserOps).GetMethod(nameof(ParserOps.RangeOperator), staticFlags);
internal static readonly MethodInfo ParserOps_GetRangeEnumerator =
typeof(ParserOps).GetMethod(nameof(ParserOps.GetRangeEnumerator), staticFlags);
internal static readonly MethodInfo ParserOps_ReplaceOperator =
typeof(ParserOps).GetMethod(nameof(ParserOps.ReplaceOperator), staticFlags);
internal static readonly MethodInfo ParserOps_SplitOperator =
Expand Down Expand Up @@ -397,9 +395,6 @@ internal static class CachedReflectionInfo
internal static readonly MethodInfo PSCreateInstanceBinder_GetTargetTypeName =
typeof(PSCreateInstanceBinder).GetMethod(nameof(PSCreateInstanceBinder.GetTargetTypeName), staticFlags);

internal static readonly ConstructorInfo RangeEnumerator_ctor =
typeof(RangeEnumerator).GetConstructor(new Type[] { typeof(int), typeof(int) });

internal static readonly MethodInfo ReservedNameMembers_GeneratePSAdaptedMemberSet =
typeof(ReservedNameMembers).GetMethod(nameof(ReservedNameMembers.GeneratePSAdaptedMemberSet), staticFlags);
internal static readonly MethodInfo ReservedNameMembers_GeneratePSBaseMemberSet =
Expand Down Expand Up @@ -4286,7 +4281,6 @@ public object VisitForEachStatement(ForEachStatementAst forEachStatementAst)

private Expression GetRangeEnumerator(ExpressionAst condExpr)
{
Expression result = null;
if (condExpr != null)
{
var binaryExpr = condExpr as BinaryExpressionAst;
Expand All @@ -4295,12 +4289,13 @@ private Expression GetRangeEnumerator(ExpressionAst condExpr)
Expression lhs = Compile(binaryExpr.Left);
Expression rhs = Compile(binaryExpr.Right);

result = Expression.New(CachedReflectionInfo.RangeEnumerator_ctor,
lhs.Convert(typeof(int)),
rhs.Convert(typeof(int)));
return Expression.Call(CachedReflectionInfo.ParserOps_GetRangeEnumerator,
lhs.Cast(typeof(object)),
rhs.Cast(typeof(object)));
}
}
return result;

return null;
}

public object VisitDoWhileStatement(DoWhileStatementAst doWhileStatementAst)
Expand Down Expand Up @@ -4874,14 +4869,13 @@ public object VisitBinaryExpression(BinaryExpressionAst binaryExpressionAst)
return Expression.Call(CachedReflectionInfo.TypeOps_AsOperator, lhs.Cast(typeof(object)), rhs.Convert(typeof(Type)));

case TokenKind.DotDot:
if(lhs.Type == typeof(string)){
return Expression.Call(CachedReflectionInfo.CharOps_Range,
lhs.Convert(typeof(char)),
rhs.Convert(typeof(char)));
}
return Expression.Call(CachedReflectionInfo.IntOps_Range,
lhs.Convert(typeof(int)),
rhs.Convert(typeof(int)));
// We could generate faster code using Expression.Dynamic with a binder.
// Currently, type checks are done in ParserOps.RangeOperator at runtime every time
// a range operator is used. By replacing with Expression.Dynamic and a binder, the
// type check is done only once when you repeatedly execute the same line in script.
return Expression.Call(
CachedReflectionInfo.ParserOps_RangeOperator, lhs.Cast(typeof(object)), rhs.Cast(typeof(object)));

case TokenKind.Multiply:
if (lhs.Type == typeof(double) && rhs.Type == typeof(double))
{
Expand Down
Loading