Commit fb9490c2 authored by Austin Clements's avatar Austin Clements

Implement slice types

R=rsc
APPROVED=rsc
DELTA=286  (217 added, 42 deleted, 27 changed)
OCL=33319
CL=33383
parent 350a8e1a
...@@ -111,6 +111,9 @@ type ArrayValue interface { ...@@ -111,6 +111,9 @@ type ArrayValue interface {
// useless Get methods, just special-case these uses. // useless Get methods, just special-case these uses.
Get() ArrayValue; Get() ArrayValue;
Elem(i int64) Value; Elem(i int64) Value;
// From returns an ArrayValue backed by the same array that
// starts from element i.
From(i int64) ArrayValue;
} }
type StructValue interface { type StructValue interface {
...@@ -126,12 +129,28 @@ type PtrValue interface { ...@@ -126,12 +129,28 @@ type PtrValue interface {
Set(Value); Set(Value);
} }
type Func interface {
NewFrame() *Frame;
Call(*Frame);
}
type FuncValue interface { type FuncValue interface {
Value; Value;
Get() Func; Get() Func;
Set(Func); Set(Func);
} }
type Slice struct {
Base ArrayValue;
Len, Cap int64;
}
type SliceValue interface {
Value;
Get() Slice;
Set(Slice);
}
/* /*
* Scopes * Scopes
*/ */
...@@ -206,12 +225,3 @@ type Frame struct { ...@@ -206,12 +225,3 @@ type Frame struct {
Outer *Frame; Outer *Frame;
Vars []Value; Vars []Value;
} }
/*
* Functions
*/
type Func interface {
NewFrame() *Frame;
Call(*Frame);
}
...@@ -37,6 +37,7 @@ type exprCompiler struct { ...@@ -37,6 +37,7 @@ type exprCompiler struct {
evalStruct func(f *Frame) StructValue; evalStruct func(f *Frame) StructValue;
evalPtr func(f *Frame) Value; evalPtr func(f *Frame) Value;
evalFunc func(f *Frame) Func; evalFunc func(f *Frame) Func;
evalSlice func(f *Frame) Slice;
evalMulti func(f *Frame) []Value; evalMulti func(f *Frame) []Value;
// Evaluate to the "address of" this value; that is, the // Evaluate to the "address of" this value; that is, the
// settable Value object. nil for expressions whose address // settable Value object. nil for expressions whose address
...@@ -164,6 +165,13 @@ func (a *exprCompiler) asFunc() (func(f *Frame) Func) { ...@@ -164,6 +165,13 @@ func (a *exprCompiler) asFunc() (func(f *Frame) Func) {
return a.evalFunc; return a.evalFunc;
} }
func (a *exprCompiler) asSlice() (func(f *Frame) Slice) {
if a.evalSlice == nil {
log.Crashf("tried to get %v node as SliceType", a.t);
}
return a.evalSlice;
}
func (a *exprCompiler) asMulti() (func(f *Frame) []Value) { func (a *exprCompiler) asMulti() (func(f *Frame) []Value) {
if a.evalMulti == nil { if a.evalMulti == nil {
log.Crashf("tried to get %v node as MultiType", a.t); log.Crashf("tried to get %v node as MultiType", a.t);
...@@ -366,6 +374,41 @@ func (a *assignCompiler) compile(lt Type) (func(lv Value, f *Frame)) { ...@@ -366,6 +374,41 @@ func (a *assignCompiler) compile(lt Type) (func(lv Value, f *Frame)) {
bad := false; bad := false;
// If this is an unpack, create a temporary to store the
// multi-value and replace the RHS with expressions to pull
// out values from the temporary. Technically, this is only
// necessary when we need to perform assignment conversions.
var effect func(f *Frame);
if isUnpack {
// TODO(austin) Is it safe to exit the block? What if
// there are multiple unpacks in one statement, such
// as for function calls?
//bc := a.rs[0].block.enterChild();
//defer bc.exit();
// This leaks a slot, but is definitely safe.
bc := a.rs[0].block;
temp := bc.DefineSlot(a.rmt);
tempIdx := temp.Index;
rf := a.rs[0].asMulti();
effect = func(f *Frame) {
f.Vars[tempIdx] = multiV(rf(f));
};
orig := a.rs[0];
a.rs = make([]*exprCompiler, len(a.rmt.Elems));
for i, t := range a.rmt.Elems {
if t.isIdeal() {
log.Crashf("Right side of unpack contains ideal: %s", rmt);
}
a.rs[i] = orig.copy();
a.rs[i].t = t;
index := i;
a.rs[i].genValue(func(f *Frame) Value { return f.Vars[tempIdx].(multiV)[index] });
}
}
// Now len(a.rs) == len(a.rmt) and we've reduced any unpacking
// to multi-assignment.
// TODO(austin) Deal with assignment special cases. This is // TODO(austin) Deal with assignment special cases. This is
// tricky in the unpack case, since some of the conversions // tricky in the unpack case, since some of the conversions
// can apply to single types within the multi-type. // can apply to single types within the multi-type.
...@@ -373,17 +416,12 @@ func (a *assignCompiler) compile(lt Type) (func(lv Value, f *Frame)) { ...@@ -373,17 +416,12 @@ func (a *assignCompiler) compile(lt Type) (func(lv Value, f *Frame)) {
// Values of any type may always be assigned to variables of // Values of any type may always be assigned to variables of
// compatible static type. // compatible static type.
for i, lt := range lmt.Elems { for i, lt := range lmt.Elems {
// Check each type individually so we can produce a
// better error message.
rt := rmt.Elems[i]; rt := rmt.Elems[i];
// When [an ideal is] (used in an expression) assigned // When [an ideal is] (used in an expression) assigned
// to a variable or typed constant, the destination // to a variable or typed constant, the destination
// must be able to represent the assigned value. // must be able to represent the assigned value.
if rt.isIdeal() { if rt.isIdeal() {
if isUnpack {
log.Crashf("Right side of unpack contains ideal: %s", rmt);
}
a.rs[i] = a.rs[i].convertTo(lmt.Elems[i]); a.rs[i] = a.rs[i].convertTo(lmt.Elems[i]);
if a.rs[i] == nil { if a.rs[i] == nil {
bad = true; bad = true;
...@@ -392,6 +430,26 @@ func (a *assignCompiler) compile(lt Type) (func(lv Value, f *Frame)) { ...@@ -392,6 +430,26 @@ func (a *assignCompiler) compile(lt Type) (func(lv Value, f *Frame)) {
rt = a.rs[i].t; rt = a.rs[i].t;
} }
// A pointer p to an array can be assigned to a slice
// variable v with compatible element type if the type
// of p or v is unnamed.
if rpt, ok := rt.lit().(*PtrType); ok {
if at, ok := rpt.Elem.lit().(*ArrayType); ok {
if lst, ok := lt.lit().(*SliceType); ok {
if lst.Elem.compat(at.Elem, false) && (rt.lit() == Type(rt) || lt.lit() == Type(lt)) {
rf := a.rs[i].asPtr();
a.rs[i] = a.rs[i].copy();
a.rs[i].t = lt;
len := at.Len;
a.rs[i].evalSlice = func(f *Frame) Slice {
return Slice{rf(f).(ArrayValue), len, len};
};
rt = a.rs[i].t;
}
}
}
}
if !lt.compat(rt, false) { if !lt.compat(rt, false) {
if len(a.rs) == 1 { if len(a.rs) == 1 {
a.rs[0].diag("illegal operand types for %s\n\t%v\n\t%v", a.errOp, lt, rt); a.rs[0].diag("illegal operand types for %s\n\t%v\n\t%v", a.errOp, lt, rt);
...@@ -406,30 +464,24 @@ func (a *assignCompiler) compile(lt Type) (func(lv Value, f *Frame)) { ...@@ -406,30 +464,24 @@ func (a *assignCompiler) compile(lt Type) (func(lv Value, f *Frame)) {
} }
// Compile // Compile
switch { if !isMT {
case !isMT:
// Case 1 // Case 1
return genAssign(lt, a.rs[0]); return genAssign(lt, a.rs[0]);
case !isUnpack: }
// Case 2 // Case 2 or 3
as := make([]func(lv Value, f *Frame), len(a.rs)); as := make([]func(lv Value, f *Frame), len(a.rs));
for i, r := range a.rs { for i, r := range a.rs {
as[i] = genAssign(lmt.Elems[i], r); as[i] = genAssign(lmt.Elems[i], r);
} }
return func(lv Value, f *Frame) { return func(lv Value, f *Frame) {
if effect != nil {
effect(f);
}
lmv := lv.(multiV); lmv := lv.(multiV);
for i, a := range as { for i, a := range as {
a(lmv[i], f); a(lmv[i], f);
} }
}; };
default:
// Case 3
rf := a.rs[0].asMulti();
return func(lv Value, f *Frame) {
lv.Assign(multiV(rf(f)));
};
}
panic();
} }
// compileAssign compiles an assignment operation without the full // compileAssign compiles an assignment operation without the full
...@@ -652,10 +704,7 @@ func (a *exprCompiler) DoSelectorExpr(x *ast.SelectorExpr) { ...@@ -652,10 +704,7 @@ func (a *exprCompiler) DoSelectorExpr(x *ast.SelectorExpr) {
// If it's a struct type, check fields and embedded types // If it's a struct type, check fields and embedded types
var builder func(*exprCompiler); var builder func(*exprCompiler);
if t, ok := t.(*StructType); ok { if t, ok := t.(*StructType); ok {
// TODO(austin) Work around := range bug for i, f := range t.Elems {
var i int;
var f StructField;
for i, f = range t.Elems {
var this *exprCompiler; var this *exprCompiler;
var sub func(*exprCompiler); var sub func(*exprCompiler);
switch { switch {
...@@ -743,10 +792,9 @@ func (a *exprCompiler) DoIndexExpr(x *ast.IndexExpr) { ...@@ -743,10 +792,9 @@ func (a *exprCompiler) DoIndexExpr(x *ast.IndexExpr) {
intIndex = true; intIndex = true;
maxIndex = lt.Len; maxIndex = lt.Len;
// TODO(austin) Uncomment when there is a SliceType case *SliceType:
// case *SliceType: at = lt.Elem;
// a.t = lt.Elem; intIndex = true;
// intIndex = true;
case *stringType: case *stringType:
at = Uint8Type; at = Uint8Type;
...@@ -802,7 +850,6 @@ func (a *exprCompiler) DoIndexExpr(x *ast.IndexExpr) { ...@@ -802,7 +850,6 @@ func (a *exprCompiler) DoIndexExpr(x *ast.IndexExpr) {
// Compile // Compile
switch lt := l.t.lit().(type) { switch lt := l.t.lit().(type) {
case *ArrayType: case *ArrayType:
a.t = lt.Elem;
// TODO(austin) Bounds check // TODO(austin) Bounds check
a.genIndexArray(l, r); a.genIndexArray(l, r);
lf := l.asArray(); lf := l.asArray();
...@@ -811,6 +858,15 @@ func (a *exprCompiler) DoIndexExpr(x *ast.IndexExpr) { ...@@ -811,6 +858,15 @@ func (a *exprCompiler) DoIndexExpr(x *ast.IndexExpr) {
return lf(f).Elem(rf(f)); return lf(f).Elem(rf(f));
}; };
case *SliceType:
// TODO(austin) Bounds check
a.genIndexSlice(l, r);
lf := l.asSlice();
rf := r.asInt();
a.evalAddr = func(f *Frame) Value {
return lf(f).Base.Elem(rf(f));
};
case *stringType: case *stringType:
// TODO(austin) Bounds check // TODO(austin) Bounds check
lf := l.asString(); lf := l.asString();
...@@ -1549,6 +1605,8 @@ func CompileExpr(scope *Scope, expr ast.Expr) (*Expr, os.Error) { ...@@ -1549,6 +1605,8 @@ func CompileExpr(scope *Scope, expr ast.Expr) (*Expr, os.Error) {
return &Expr{t, func(f *Frame, out Value) { out.(PtrValue).Set(ec.evalPtr(f)) }}, nil; return &Expr{t, func(f *Frame, out Value) { out.(PtrValue).Set(ec.evalPtr(f)) }}, nil;
case *FuncType: case *FuncType:
return &Expr{t, func(f *Frame, out Value) { out.(FuncValue).Set(ec.evalFunc(f)) }}, nil; return &Expr{t, func(f *Frame, out Value) { out.(FuncValue).Set(ec.evalFunc(f)) }}, nil;
case *SliceType:
return &Expr{t, func(f *Frame, out Value) { out.(SliceValue).Set(ec.evalSlice(f)) }}, nil;
} }
log.Crashf("unexpected type %v", ec.t); log.Crashf("unexpected type %v", ec.t);
panic(); panic();
...@@ -1594,6 +1652,9 @@ func (a *exprCompiler) genConstant(v Value) { ...@@ -1594,6 +1652,9 @@ func (a *exprCompiler) genConstant(v Value) {
case *FuncType: case *FuncType:
val := v.(FuncValue).Get(); val := v.(FuncValue).Get();
a.evalFunc = func(f *Frame) Func { return val }; a.evalFunc = func(f *Frame) Func { return val };
case *SliceType:
val := v.(SliceValue).Get();
a.evalSlice = func(f *Frame) Slice { return val };
default: default:
log.Crashf("unexpected constant type %v at %v", a.t, a.pos); log.Crashf("unexpected constant type %v at %v", a.t, a.pos);
} }
...@@ -1620,6 +1681,8 @@ func (a *exprCompiler) genIdentOp(level int, index int) { ...@@ -1620,6 +1681,8 @@ func (a *exprCompiler) genIdentOp(level int, index int) {
a.evalPtr = func(f *Frame) Value { return f.Get(level, index).(PtrValue).Get() }; a.evalPtr = func(f *Frame) Value { return f.Get(level, index).(PtrValue).Get() };
case *FuncType: case *FuncType:
a.evalFunc = func(f *Frame) Func { return f.Get(level, index).(FuncValue).Get() }; a.evalFunc = func(f *Frame) Func { return f.Get(level, index).(FuncValue).Get() };
case *SliceType:
a.evalSlice = func(f *Frame) Slice { return f.Get(level, index).(SliceValue).Get() };
default: default:
log.Crashf("unexpected identifier type %v at %v", a.t, a.pos); log.Crashf("unexpected identifier type %v at %v", a.t, a.pos);
} }
...@@ -1647,6 +1710,37 @@ func (a *exprCompiler) genIndexArray(l *exprCompiler, r *exprCompiler) { ...@@ -1647,6 +1710,37 @@ func (a *exprCompiler) genIndexArray(l *exprCompiler, r *exprCompiler) {
a.evalPtr = func(f *Frame) Value { return lf(f).Elem(rf(f)).(PtrValue).Get() }; a.evalPtr = func(f *Frame) Value { return lf(f).Elem(rf(f)).(PtrValue).Get() };
case *FuncType: case *FuncType:
a.evalFunc = func(f *Frame) Func { return lf(f).Elem(rf(f)).(FuncValue).Get() }; a.evalFunc = func(f *Frame) Func { return lf(f).Elem(rf(f)).(FuncValue).Get() };
case *SliceType:
a.evalSlice = func(f *Frame) Slice { return lf(f).Elem(rf(f)).(SliceValue).Get() };
default:
log.Crashf("unexpected result type %v at %v", a.t, a.pos);
}
}
func (a *exprCompiler) genIndexSlice(l *exprCompiler, r *exprCompiler) {
lf := l.asSlice();
rf := r.asInt();
switch _ := a.t.lit().(type) {
case *boolType:
a.evalBool = func(f *Frame) bool { return lf(f).Base.Elem(rf(f)).(BoolValue).Get() };
case *uintType:
a.evalUint = func(f *Frame) uint64 { return lf(f).Base.Elem(rf(f)).(UintValue).Get() };
case *intType:
a.evalInt = func(f *Frame) int64 { return lf(f).Base.Elem(rf(f)).(IntValue).Get() };
case *floatType:
a.evalFloat = func(f *Frame) float64 { return lf(f).Base.Elem(rf(f)).(FloatValue).Get() };
case *stringType:
a.evalString = func(f *Frame) string { return lf(f).Base.Elem(rf(f)).(StringValue).Get() };
case *ArrayType:
a.evalArray = func(f *Frame) ArrayValue { return lf(f).Base.Elem(rf(f)).(ArrayValue).Get() };
case *StructType:
a.evalStruct = func(f *Frame) StructValue { return lf(f).Base.Elem(rf(f)).(StructValue).Get() };
case *PtrType:
a.evalPtr = func(f *Frame) Value { return lf(f).Base.Elem(rf(f)).(PtrValue).Get() };
case *FuncType:
a.evalFunc = func(f *Frame) Func { return lf(f).Base.Elem(rf(f)).(FuncValue).Get() };
case *SliceType:
a.evalSlice = func(f *Frame) Slice { return lf(f).Base.Elem(rf(f)).(SliceValue).Get() };
default: default:
log.Crashf("unexpected result type %v at %v", a.t, a.pos); log.Crashf("unexpected result type %v at %v", a.t, a.pos);
} }
...@@ -1673,6 +1767,8 @@ func (a *exprCompiler) genFuncCall(call func(f *Frame) []Value) { ...@@ -1673,6 +1767,8 @@ func (a *exprCompiler) genFuncCall(call func(f *Frame) []Value) {
a.evalPtr = func(f *Frame) Value { return call(f)[0].(PtrValue).Get() }; a.evalPtr = func(f *Frame) Value { return call(f)[0].(PtrValue).Get() };
case *FuncType: case *FuncType:
a.evalFunc = func(f *Frame) Func { return call(f)[0].(FuncValue).Get() }; a.evalFunc = func(f *Frame) Func { return call(f)[0].(FuncValue).Get() };
case *SliceType:
a.evalSlice = func(f *Frame) Slice { return call(f)[0].(SliceValue).Get() };
case *MultiType: case *MultiType:
a.evalMulti = func(f *Frame) []Value { return call(f) }; a.evalMulti = func(f *Frame) []Value { return call(f) };
default: default:
...@@ -1701,6 +1797,8 @@ func (a *exprCompiler) genValue(vf func(*Frame) Value) { ...@@ -1701,6 +1797,8 @@ func (a *exprCompiler) genValue(vf func(*Frame) Value) {
a.evalPtr = func(f *Frame) Value { return vf(f).(PtrValue).Get() }; a.evalPtr = func(f *Frame) Value { return vf(f).(PtrValue).Get() };
case *FuncType: case *FuncType:
a.evalFunc = func(f *Frame) Func { return vf(f).(FuncValue).Get() }; a.evalFunc = func(f *Frame) Func { return vf(f).(FuncValue).Get() };
case *SliceType:
a.evalSlice = func(f *Frame) Slice { return vf(f).(SliceValue).Get() };
default: default:
log.Crashf("unexpected result type %v at %v", a.t, a.pos); log.Crashf("unexpected result type %v at %v", a.t, a.pos);
} }
...@@ -2249,12 +2347,18 @@ func genAssign(lt Type, r *exprCompiler) (func(lv Value, f *Frame)) { ...@@ -2249,12 +2347,18 @@ func genAssign(lt Type, r *exprCompiler) (func(lv Value, f *Frame)) {
case *ArrayType: case *ArrayType:
rf := r.asArray(); rf := r.asArray();
return func(lv Value, f *Frame) { lv.Assign(rf(f)) }; return func(lv Value, f *Frame) { lv.Assign(rf(f)) };
case *StructType:
rf := r.asStruct();
return func(lv Value, f *Frame) { lv.Assign(rf(f)) };
case *PtrType: case *PtrType:
rf := r.asPtr(); rf := r.asPtr();
return func(lv Value, f *Frame) { lv.(PtrValue).Set(rf(f)) }; return func(lv Value, f *Frame) { lv.(PtrValue).Set(rf(f)) };
case *FuncType: case *FuncType:
rf := r.asFunc(); rf := r.asFunc();
return func(lv Value, f *Frame) { lv.(FuncValue).Set(rf(f)) }; return func(lv Value, f *Frame) { lv.(FuncValue).Set(rf(f)) };
case *SliceType:
rf := r.asSlice();
return func(lv Value, f *Frame) { lv.(SliceValue).Set(rf(f)) };
default: default:
log.Crashf("unexpected left operand type %v at %v", lt, r.pos); log.Crashf("unexpected left operand type %v at %v", lt, r.pos);
} }
......
...@@ -579,7 +579,6 @@ type StructField struct { ...@@ -579,7 +579,6 @@ type StructField struct {
type StructType struct { type StructType struct {
commonType; commonType;
Elems []StructField; Elems []StructField;
maxDepth int;
} }
var structTypes = newTypeArrayMap() var structTypes = newTypeArrayMap()
...@@ -626,19 +625,7 @@ func NewStructType(fields []StructField) *StructType { ...@@ -626,19 +625,7 @@ func NewStructType(fields []StructField) *StructType {
t, ok := tMap[key]; t, ok := tMap[key];
if !ok { if !ok {
// Create new struct type // Create new struct type
t = &StructType{commonType{}, fields};
// Compute max anonymous field depth
maxDepth := 1;
for _, f := range fields {
// TODO(austin) Careful of type T struct { *T }
if st, ok := f.Type.(*StructType); ok {
if st.maxDepth + 1 > maxDepth {
maxDepth = st.maxDepth + 1;
}
}
}
t = &StructType{commonType{}, fields, maxDepth};
tMap[key] = t; tMap[key] = t;
} }
return t; return t;
...@@ -870,11 +857,47 @@ func (t *FuncDecl) String() string { ...@@ -870,11 +857,47 @@ func (t *FuncDecl) String() string {
type InterfaceType struct { type InterfaceType struct {
// TODO(austin) // TODO(austin)
} }
*/
type SliceType struct { type SliceType struct {
// TODO(austin) commonType;
Elem Type;
}
var sliceTypes = make(map[Type] *SliceType)
// Two slice types are identical if they have identical element types.
func NewSliceType(elem Type) *SliceType {
t, ok := sliceTypes[elem];
if !ok {
t = &SliceType{commonType{}, elem};
sliceTypes[elem] = t;
}
return t;
}
func (t *SliceType) compat(o Type, conv bool) bool {
t2, ok := o.lit().(*SliceType);
if !ok {
return false;
}
return t.Elem.compat(t2.Elem, conv);
}
func (t *SliceType) lit() Type {
return t;
}
func (t *SliceType) String() string {
return "[]" + t.Elem.String();
}
func (t *SliceType) Zero() Value {
return &sliceV{Slice{nil, 0, 0}};
} }
/*
type MapType struct { type MapType struct {
// TODO(austin) // TODO(austin)
} }
......
...@@ -56,20 +56,22 @@ func (a *typeCompiler) compileIdent(x *ast.Ident, allowRec bool) Type { ...@@ -56,20 +56,22 @@ func (a *typeCompiler) compileIdent(x *ast.Ident, allowRec bool) Type {
} }
func (a *typeCompiler) compileArrayType(x *ast.ArrayType, allowRec bool) Type { func (a *typeCompiler) compileArrayType(x *ast.ArrayType, allowRec bool) Type {
// Compile element type
elem := a.compileType(x.Elt, allowRec);
// Compile length expression // Compile length expression
if x.Len == nil { if x.Len == nil {
a.diagAt(x, "slice types not implemented"); if elem == nil {
return nil; return nil;
} }
return NewSliceType(elem);
}
if _, ok := x.Len.(*ast.Ellipsis); ok { if _, ok := x.Len.(*ast.Ellipsis); ok {
a.diagAt(x.Len, "... array initailizers not implemented"); a.diagAt(x.Len, "... array initailizers not implemented");
return nil; return nil;
} }
l, ok := a.compileArrayLen(a.block, x.Len); l, ok := a.compileArrayLen(a.block, x.Len);
// Compile element type
elem := a.compileType(x.Elt, allowRec);
if !ok { if !ok {
return nil; return nil;
} }
......
...@@ -383,6 +383,11 @@ func (v *arrayV) Elem(i int64) Value { ...@@ -383,6 +383,11 @@ func (v *arrayV) Elem(i int64) Value {
return (*v)[i]; return (*v)[i];
} }
func (v *arrayV) From(i int64) ArrayValue {
res := (*v)[i:len(*v)];
return &res;
}
/* /*
* Struct * Struct
*/ */
...@@ -468,6 +473,37 @@ func (v *funcV) Set(x Func) { ...@@ -468,6 +473,37 @@ func (v *funcV) Set(x Func) {
v.target = x; v.target = x;
} }
/*
* Slices
*/
type sliceV struct {
Slice;
}
func (v *sliceV) String() string {
res := "{";
for i := int64(0); i < v.Len; i++ {
if i > 0 {
res += ", ";
}
res += v.Base.Elem(i).String();
}
return res + "}";
}
func (v *sliceV) Assign(o Value) {
v.Slice = o.(SliceValue).Get();
}
func (v *sliceV) Get() Slice {
return v.Slice;
}
func (v *sliceV) Set(x Slice) {
v.Slice = x;
}
/* /*
* Multi-values * Multi-values
*/ */
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment