Skip to content

Commit

Permalink
array spread operator for array initializers
Browse files Browse the repository at this point in the history
implements #1102
  • Loading branch information
jakubmisek committed May 4, 2024
1 parent 99d1ce9 commit 743b63e
Show file tree
Hide file tree
Showing 9 changed files with 196 additions and 68 deletions.
23 changes: 16 additions & 7 deletions src/Peachpie.CodeAnalysis/CodeGen/Graph/BoundExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2816,9 +2816,10 @@ internal TypeSymbol EmitMagicCall(CodeGenerator cg, string originalMethodName, M
// $arguments: PhpArray
BoundArgument.Create(
new BoundArrayEx(
_arguments.Select(
arg => new KeyValuePair<BoundExpression, BoundExpression>(null, arg.Value)
).ToImmutableArray())
_arguments.SelectAsArray(
arg => new BoundArrayEx.BoundArrayItem(null, arg.Value)
)
)
.WithAccess(BoundAccess.Read))
);
}
Expand Down Expand Up @@ -4091,7 +4092,7 @@ partial class BoundArrayEx
{
internal override TypeSymbol Emit(CodeGenerator cg)
{
if (_items.Length == 0)
if (this.Items.IsDefaultOrEmpty)
{
if (Access.IsNone && !cg.EmitPdbSequencePoints)
{
Expand Down Expand Up @@ -4122,18 +4123,26 @@ internal override TypeSymbol Emit(CodeGenerator cg)
TypeSymbol EmitNewPhpArray(CodeGenerator cg)
{
// new PhpArray(count)
cg.Builder.EmitIntConstant(_items.Length);
cg.Builder.EmitIntConstant(this.Items.Length);
var result = cg.EmitCall(ILOpCode.Newobj, cg.CoreMethods.Ctors.PhpArray_int)
.Expect(cg.CoreTypes.PhpArray);

foreach (var x in _items)
foreach (var x in this.Items)
{
Debug.Assert(x.Value != null);

// <PhpArray>
cg.Builder.EmitOpCode(ILOpCode.Dup);

if (x.Key == null)
if (x.IsSpreadArray)
{
Debug.Assert(x.Key == null);

// Operators.AddRange(<stack>, value)
cg.EmitConvertToPhpValue(x.Value);
cg.EmitPop(cg.EmitCall(ILOpCode.Call, cg.CoreMethods.Operators.AddRange_PhpArray_PhpValue));
}
else if (x.Key == null)
{
// <stack>.Add(value) : int
cg.EmitConvertToPhpValue(x.Value);
Expand Down
59 changes: 34 additions & 25 deletions src/Peachpie.CodeAnalysis/Semantics/BoundExpression.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1975,7 +1975,7 @@ public class BoundArrayInitializer : BoundExpression, IArrayInitializerOperation

public override bool IsDeeplyCopied => false;

ImmutableArray<IOperation> IArrayInitializerOperation.ElementValues => _array._items.Select(x => x.Value).Cast<IOperation>().AsImmutable();
ImmutableArray<IOperation> IArrayInitializerOperation.ElementValues => _array.Items.Select(x => x.Value).Cast<IOperation>().AsImmutable();

public BoundArrayInitializer(BoundArrayEx array)
{
Expand All @@ -1984,14 +1984,9 @@ public BoundArrayInitializer(BoundArrayEx array)

public BoundArrayInitializer Update(BoundArrayEx array)
{
if (array == _array)
{
return this;
}
else
{
return new BoundArrayInitializer(array).WithContext(this);
}
return array != _array
? new BoundArrayInitializer(array).WithContext(this)
: this;
}

public override void Accept(OperationVisitor visitor)
Expand All @@ -2009,37 +2004,51 @@ public override TResult Accept<TResult>(PhpOperationVisitor<TResult> visitor)
}
}

public struct BoundArrayItem
{
public readonly BoundExpression Key;

public readonly BoundExpression Value;

/// <summary>
/// Flag indicating the <see cref="Value"/> is spread using <c>...</c> operator.
/// </summary>
public bool IsSpreadArray;

public bool RequiresContext => (Key != null && Key.RequiresContext) || Value.RequiresContext;

public BoundArrayItem(BoundExpression key, BoundExpression value)
{
Key = key;
Value = value ?? throw new ArgumentNullException(nameof(value));
}
}

public override bool IsDeeplyCopied => false; // Emit() always creates an instance that does not need to be deepcopied again

public override OperationKind Kind => OperationKind.ArrayCreation;

public override bool RequiresContext => _items.Any(x => (x.Key != null && x.Key.RequiresContext) || x.Value.RequiresContext);
public override bool RequiresContext => this.Items.Any(x => (x.Key != null && x.Key.RequiresContext) || x.Value.RequiresContext);

ImmutableArray<IOperation> IArrayCreationOperation.DimensionSizes => ImmutableArray.Create<IOperation>(new BoundLiteral(_items.Length));
ImmutableArray<IOperation> IArrayCreationOperation.DimensionSizes => ImmutableArray.Create<IOperation>(new BoundLiteral(this.Items.Length));

IArrayInitializerOperation IArrayCreationOperation.Initializer => new BoundArrayInitializer(this);

/// <summary>
/// Array items.
/// Bound array items.
/// </summary>
public ImmutableArray<KeyValuePair<BoundExpression, BoundExpression>> Items { get => _items; internal set => _items = value; }
ImmutableArray<KeyValuePair<BoundExpression, BoundExpression>> _items;
public ImmutableArray<BoundArrayItem> Items { get; internal set; }

public BoundArrayEx(ImmutableArray<KeyValuePair<BoundExpression, BoundExpression>> items)
public BoundArrayEx(ImmutableArray<BoundArrayItem> items)
{
_items = items;
this.Items = items;
}

public BoundArrayEx Update(ImmutableArray<KeyValuePair<BoundExpression, BoundExpression>> items)
public BoundArrayEx Update(ImmutableArray<BoundArrayItem> items)
{
if (items == _items)
{
return this;
}
else
{
return new BoundArrayEx(items).WithContext(this);
}
return items != this.Items
? new BoundArrayEx(items).WithContext(this)
: this;
}

public override void Accept(OperationVisitor visitor)
Expand Down
34 changes: 33 additions & 1 deletion src/Peachpie.CodeAnalysis/Semantics/Graph/GraphUpdater.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,38 @@ public abstract class GraphUpdater : GraphVisitor<object>
return alternate?.MoveToImmutable() ?? arr;
}

protected ImmutableArray<BoundArrayEx.BoundArrayItem> VisitArrayItemArray(ImmutableArray<BoundArrayEx.BoundArrayItem> arr)
{
if (arr.IsDefaultOrEmpty)
{
return arr;
}

ImmutableArray<BoundArrayEx.BoundArrayItem>.Builder alternate = null;

for (int i = 0; i < arr.Length; i++)
{
var orig = arr[i];
var visitedKey = (BoundExpression)orig.Key?.Accept(this);
var visitedValue = (BoundExpression)orig.Value?.Accept(this);

if (visitedKey != orig.Key || visitedValue != orig.Value)
{
if (alternate == null)
{
alternate = arr.ToBuilder();
}

alternate[i] = new BoundArrayEx.BoundArrayItem(visitedKey, visitedValue)
{
IsSpreadArray = orig.IsSpreadArray,
};
}
}

return alternate?.MoveToImmutable() ?? arr;
}

protected ImmutableArray<KeyValuePair<T1, T2>> VisitImmutableArrayPairs<T1, T2>(ImmutableArray<KeyValuePair<T1, T2>> arr)
where T1 : BoundOperation, IPhpOperation
where T2 : BoundOperation, IPhpOperation
Expand Down Expand Up @@ -483,7 +515,7 @@ public override object VisitFieldRef(BoundFieldRef x)

public override object VisitArray(BoundArrayEx x)
{
return x.Update(VisitImmutableArrayPairs(x.Items));
return x.Update(VisitArrayItemArray(x.Items));
}

public override object VisitArrayItem(BoundArrayItemEx x)
Expand Down
47 changes: 20 additions & 27 deletions src/Peachpie.CodeAnalysis/Semantics/SemanticsBinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Pchp.CodeAnalysis.FlowAnalysis;
using Pchp.CodeAnalysis.Semantics.TypeRef;
using Peachpie.CodeAnalysis.Syntax;
using BoundArrayItem = Pchp.CodeAnalysis.Semantics.BoundArrayEx.BoundArrayItem;

namespace Pchp.CodeAnalysis.Semantics
{
Expand Down Expand Up @@ -1081,53 +1082,45 @@ protected BoundExpression BindArrayEx(AST.ArrayEx x, BoundAccess access)
return builder.MoveToImmutable();
}

protected ImmutableArray<KeyValuePair<BoundExpression, BoundExpression>> BindArrayItems(AST.Item[] items)
protected ImmutableArray<BoundArrayItem> BindArrayItems(ReadOnlySpan<AST.Item> items)
{
// trim trailing empty items
int count = items.Length;
while (count > 0 && items[count - 1] == null)
while (items.Length != 0 && items[items.Length - 1] == null)
{
count--;
items = items.Slice(0, items.Length - 1);
}

if (count == 0)
if (items.IsEmpty)
{
return ImmutableArray<KeyValuePair<BoundExpression, BoundExpression>>.Empty;
return ImmutableArray<BoundArrayItem>.Empty;
}

var builder = ImmutableArray.CreateBuilder<KeyValuePair<BoundExpression, BoundExpression>>(count);
var builder = ImmutableArray.CreateBuilder<BoundArrayItem>(items.Length);

for (int i = 0; i < count; i++)
foreach (var x in items)
{
var x = items[i];
if (x == null)
{
throw ExceptionUtilities.Unreachable;
}

if (x is AST.SpreadItem)
{
Diagnostics.Add(
ContainingFile.GetLocation(x.Value.Span.ToTextSpan()),
Errors.ErrorCode.ERR_NotYetImplemented,
"'...' spread array operator");
continue;
}

Debug.Assert(x is AST.RefItem || x is AST.ValueItem);

var boundIndex = (x.Index != null) ? BindExpression(x.Index, BoundAccess.Read) : null;
var value = (AST.Expression)((AST.IArrayItem)x).Value;

// read access
var boundValue = BindExpression(value, x.IsByRef ? BoundAccess.ReadRef : BoundAccess.Read);
// bind key, value
var boundIndex = x.Index != null ? BindExpression(x.Index, BoundAccess.Read) : null;
var boundValue = BindExpression((AST.Expression)x.Value, x.IsByRef ? BoundAccess.ReadRef : BoundAccess.Read);
var isSpreadArray = x is AST.SpreadItem;

if (!x.IsByRef)
if (!x.IsByRef && !isSpreadArray)
{
boundValue = BindCopyValue(boundValue);
}

builder.Add(new KeyValuePair<BoundExpression, BoundExpression>(boundIndex, boundValue));
//
builder.Add(
new BoundArrayItem(boundIndex, boundValue)
{
IsSpreadArray = isSpreadArray
}
);
}

//
Expand Down
2 changes: 2 additions & 0 deletions src/Peachpie.CodeAnalysis/Symbols/CoreMembers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,6 +324,7 @@ public OperatorsHolder(CoreTypes ct)
EnsureItemAlias_IPhpArray_PhpValue_Bool = ct.Operators.Method("EnsureItemAlias", ct.IPhpArray, ct.PhpValue, ct.Boolean);
EnsureItemArray_IPhpArray_PhpValue = ct.Operators.Method("EnsureItemArray", ct.IPhpArray, ct.PhpValue);
EnsureItemObject_IPhpArray_PhpValue = ct.Operators.Method("EnsureItemObject", ct.IPhpArray, ct.PhpValue);
AddRange_PhpArray_PhpValue = ct.Operators.Method("AddRange", ct.PhpArray, ct.PhpValue);
IsSet_PhpValue = ct.Operators.Method("IsSet", ct.PhpValue);
IsEmpty_PhpValue = ct.Operators.Method("IsEmpty", ct.PhpValue);
IsNullOrEmpty_String = ct.String.Method("IsNullOrEmpty", ct.String);
Expand Down Expand Up @@ -471,6 +472,7 @@ public readonly CoreMethod
EnsureItemAlias_IPhpArray_PhpValue_Bool, EnsureItemAlias_PhpValue_PhpValue_Bool,
EnsureItemArray_IPhpArray_PhpValue,
EnsureItemObject_IPhpArray_PhpValue,
AddRange_PhpArray_PhpValue,
IsSet_PhpValue, IsEmpty_PhpValue, IsNullOrEmpty_String, Concat_String_String,
ToBoolean_String, ToBoolean_PhpString,
ToString_Bool, ToString_Long, ToString_Int32, ToString_Double, Long_ToString,
Expand Down
66 changes: 65 additions & 1 deletion src/Peachpie.Runtime/Operators.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1936,7 +1936,7 @@ public static OrderedDictionary.FastEnumerator GetFastEnumerator(PhpArray array,

#endregion

#region Copy, Unpack
#region Copy, Unpack, AddRange

/// <summary>
/// Gets copy of given value.
Expand Down Expand Up @@ -2190,6 +2190,70 @@ public static void Unpack(List<PhpValue> stack, Traversable traversable, ulong b
}
}

/// <summary>
/// Add values from <paramref name="spread"/>, expecting it to be an array or <see cref="Traversable"/>.
/// Implements spread operator <c>...</c>.
/// </summary>
/// <param name="array">Target array object.</param>
/// <param name="spread"><see cref="PhpArray"/>, <see cref="Traversable"/>, or <see cref="IEnumerable"/>.</param>
/// <remarks>
/// Throws error exception if <paramref name="spread"/> is not foreachable.
/// </remarks>
public static void AddRange(this PhpArray array, PhpValue spread)
{
if (array == null)
{
throw new ArgumentNullException(nameof(array));
}

switch (spread.TypeCode)
{
case PhpTypeCode.PhpArray:
AddRange(array, other: spread.Array);
break;
case PhpTypeCode.Object:

var e = GetForeachEnumerator(spread.Object, false, default(RuntimeTypeHandle));
while (e.MoveNext())
{
var value = e.CurrentValue.DeepCopy();
if (e.CurrentKey.TryToIntStringKey(out var key) && key.IsString)
{
array[key] = value;
}
else
{
array.AddValue(value);
}
}

break;
default:
PhpException.Throw(PhpError.E_ERROR, ErrResources.unpack_argument_error, PhpVariable.GetTypeName(spread));
break;
}
}

static void AddRange(this PhpArray array, PhpArray other)
{
// only string keys are preserved

var e = other.GetFastEnumerator();
while (e.MoveNext())
{
var value = e.CurrentValue.DeepCopy();
var key = e.CurrentKey;
if (key.IsString)
{
array[key] = value;
}
else
{
array.AddValue(value);
}
}
}

#endregion

#region ReadConstant
Expand Down

0 comments on commit 743b63e

Please sign in to comment.