mirror of
https://github.com/golang/go
synced 2024-09-05 07:54:46 +00:00
[dev.typeparams] cmd/compile/internal/inline: refactor mkinlcall
This CL refactors mkinlcall by extracting the core InlinedCallExpr construction code into a new "oldInline" function, and adds a new "NewInline" hook point that can be overriden with a new inliner implementation that only needs to worry about the details of constructing the InlinedCallExpr. It also moves the delayretvars optimization check into CanInline, so it's performed just once per inlinable function rather than once for each inlined call. Finally, it skips printing the function body about to be inlined (and updates the couple of regress tests that expected this output). We already report the inline body as it was saved, and this diagnostic is only applicable to the current inliner, which clones existing function body instances. In the unified IR inliner, we'll directly construct inline bodies from the serialized representation. Change-Id: Ibdbe617da83c07665dcbda402cc8d4d4431dde2f Reviewed-on: https://go-review.googlesource.com/c/go/+/323290 Trust: Matthew Dempsky <mdempsky@google.com> Trust: Dan Scales <danscales@google.com> Run-TryBot: Matthew Dempsky <mdempsky@google.com> TryBot-Result: Go Bot <gobot@golang.org> Reviewed-by: Dan Scales <danscales@google.com>
This commit is contained in:
parent
88583a2a66
commit
417955d151
|
@ -179,6 +179,8 @@ func CanInline(fn *ir.Func) {
|
||||||
Cost: inlineMaxBudget - visitor.budget,
|
Cost: inlineMaxBudget - visitor.budget,
|
||||||
Dcl: pruneUnusedAutos(n.Defn.(*ir.Func).Dcl, &visitor),
|
Dcl: pruneUnusedAutos(n.Defn.(*ir.Func).Dcl, &visitor),
|
||||||
Body: inlcopylist(fn.Body),
|
Body: inlcopylist(fn.Body),
|
||||||
|
|
||||||
|
CanDelayResults: canDelayResults(fn),
|
||||||
}
|
}
|
||||||
|
|
||||||
if base.Flag.LowerM > 1 {
|
if base.Flag.LowerM > 1 {
|
||||||
|
@ -191,6 +193,38 @@ func CanInline(fn *ir.Func) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// canDelayResults reports whether inlined calls to fn can delay
|
||||||
|
// declaring the result parameter until the "return" statement.
|
||||||
|
func canDelayResults(fn *ir.Func) bool {
|
||||||
|
// We can delay declaring+initializing result parameters if:
|
||||||
|
// (1) there's exactly one "return" statement in the inlined function;
|
||||||
|
// (2) it's not an empty return statement (#44355); and
|
||||||
|
// (3) the result parameters aren't named.
|
||||||
|
|
||||||
|
nreturns := 0
|
||||||
|
ir.VisitList(fn.Body, func(n ir.Node) {
|
||||||
|
if n, ok := n.(*ir.ReturnStmt); ok {
|
||||||
|
nreturns++
|
||||||
|
if len(n.Results) == 0 {
|
||||||
|
nreturns++ // empty return statement (case 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if nreturns != 1 {
|
||||||
|
return false // not exactly one return statement (case 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// temporaries for return values.
|
||||||
|
for _, param := range fn.Type().Results().FieldSlice() {
|
||||||
|
if sym := types.OrigSym(param.Sym); sym != nil && !sym.IsBlank() {
|
||||||
|
return false // found a named result parameter (case 3)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Inline_Flood marks n's inline body for export and recursively ensures
|
// Inline_Flood marks n's inline body for export and recursively ensures
|
||||||
// all called functions are marked too.
|
// all called functions are marked too.
|
||||||
func Inline_Flood(n *ir.Name, exportsym func(*ir.Name)) {
|
func Inline_Flood(n *ir.Name, exportsym func(*ir.Name)) {
|
||||||
|
@ -740,6 +774,11 @@ var inlgen int
|
||||||
// when producing output for debugging the compiler itself.
|
// when producing output for debugging the compiler itself.
|
||||||
var SSADumpInline = func(*ir.Func) {}
|
var SSADumpInline = func(*ir.Func) {}
|
||||||
|
|
||||||
|
// NewInline allows the inliner implementation to be overridden.
|
||||||
|
// If it returns nil, the legacy inliner will handle this call
|
||||||
|
// instead.
|
||||||
|
var NewInline = func(call *ir.CallExpr, fn *ir.Func, inlIndex int) *ir.InlinedCallExpr { return nil }
|
||||||
|
|
||||||
// If n is a call node (OCALLFUNC or OCALLMETH), and fn is an ONAME node for a
|
// If n is a call node (OCALLFUNC or OCALLMETH), and fn is an ONAME node for a
|
||||||
// function with an inlinable body, return an OINLCALL node that can replace n.
|
// function with an inlinable body, return an OINLCALL node that can replace n.
|
||||||
// The returned node's Ninit has the parameter assignments, the Nbody is the
|
// The returned node's Ninit has the parameter assignments, the Nbody is the
|
||||||
|
@ -796,30 +835,64 @@ func mkinlcall(n *ir.CallExpr, fn *ir.Func, maxCost int32, inlMap map[*ir.Func]b
|
||||||
|
|
||||||
typecheck.FixVariadicCall(n)
|
typecheck.FixVariadicCall(n)
|
||||||
|
|
||||||
if base.Debug.TypecheckInl == 0 {
|
parent := base.Ctxt.PosTable.Pos(n.Pos()).Base().InliningIndex()
|
||||||
typecheck.ImportedBody(fn)
|
|
||||||
|
sym := fn.Linksym()
|
||||||
|
inlIndex := base.Ctxt.InlTree.Add(parent, n.Pos(), sym)
|
||||||
|
|
||||||
|
if base.Flag.GenDwarfInl > 0 {
|
||||||
|
if !sym.WasInlined() {
|
||||||
|
base.Ctxt.DwFixups.SetPrecursorFunc(sym, fn)
|
||||||
|
sym.Set(obj.AttrWasInlined, true)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We have a function node, and it has an inlineable body.
|
if base.Flag.LowerM != 0 {
|
||||||
if base.Flag.LowerM > 1 {
|
|
||||||
fmt.Printf("%v: inlining call to %v %v { %v }\n", ir.Line(n), fn.Sym(), fn.Type(), ir.Nodes(fn.Inl.Body))
|
|
||||||
} else if base.Flag.LowerM != 0 {
|
|
||||||
fmt.Printf("%v: inlining call to %v\n", ir.Line(n), fn)
|
fmt.Printf("%v: inlining call to %v\n", ir.Line(n), fn)
|
||||||
}
|
}
|
||||||
if base.Flag.LowerM > 2 {
|
if base.Flag.LowerM > 2 {
|
||||||
fmt.Printf("%v: Before inlining: %+v\n", ir.Line(n), n)
|
fmt.Printf("%v: Before inlining: %+v\n", ir.Line(n), n)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
res := NewInline(n, fn, inlIndex)
|
||||||
|
if res == nil {
|
||||||
|
res = oldInline(n, fn, inlIndex)
|
||||||
|
}
|
||||||
|
|
||||||
|
// transitive inlining
|
||||||
|
// might be nice to do this before exporting the body,
|
||||||
|
// but can't emit the body with inlining expanded.
|
||||||
|
// instead we emit the things that the body needs
|
||||||
|
// and each use must redo the inlining.
|
||||||
|
// luckily these are small.
|
||||||
|
ir.EditChildren(res, edit)
|
||||||
|
|
||||||
|
if base.Flag.LowerM > 2 {
|
||||||
|
fmt.Printf("%v: After inlining %+v\n\n", ir.Line(res), res)
|
||||||
|
}
|
||||||
|
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// oldInline creates an InlinedCallExpr to replace the given call
|
||||||
|
// expression. fn is the callee function to be inlined. inlIndex is
|
||||||
|
// the inlining tree position index, for use with src.NewInliningBase
|
||||||
|
// when rewriting positions.
|
||||||
|
func oldInline(call *ir.CallExpr, fn *ir.Func, inlIndex int) *ir.InlinedCallExpr {
|
||||||
|
if base.Debug.TypecheckInl == 0 {
|
||||||
|
typecheck.ImportedBody(fn)
|
||||||
|
}
|
||||||
|
|
||||||
SSADumpInline(fn)
|
SSADumpInline(fn)
|
||||||
|
|
||||||
ninit := n.Init()
|
ninit := call.Init()
|
||||||
|
|
||||||
// For normal function calls, the function callee expression
|
// For normal function calls, the function callee expression
|
||||||
// may contain side effects (e.g., added by addinit during
|
// may contain side effects (e.g., added by addinit during
|
||||||
// inlconv2expr or inlconv2list). Make sure to preserve these,
|
// inlconv2expr or inlconv2list). Make sure to preserve these,
|
||||||
// if necessary (#42703).
|
// if necessary (#42703).
|
||||||
if n.Op() == ir.OCALLFUNC {
|
if call.Op() == ir.OCALLFUNC {
|
||||||
callee := n.X
|
callee := call.X
|
||||||
for callee.Op() == ir.OCONVNOP {
|
for callee.Op() == ir.OCONVNOP {
|
||||||
conv := callee.(*ir.ConvExpr)
|
conv := callee.(*ir.ConvExpr)
|
||||||
ninit.Append(ir.TakeInit(conv)...)
|
ninit.Append(ir.TakeInit(conv)...)
|
||||||
|
@ -857,25 +930,6 @@ func mkinlcall(n *ir.CallExpr, fn *ir.Func, maxCost int32, inlMap map[*ir.Func]b
|
||||||
}
|
}
|
||||||
|
|
||||||
// We can delay declaring+initializing result parameters if:
|
// We can delay declaring+initializing result parameters if:
|
||||||
// (1) there's exactly one "return" statement in the inlined function;
|
|
||||||
// (2) it's not an empty return statement (#44355); and
|
|
||||||
// (3) the result parameters aren't named.
|
|
||||||
delayretvars := true
|
|
||||||
|
|
||||||
nreturns := 0
|
|
||||||
ir.VisitList(ir.Nodes(fn.Inl.Body), func(n ir.Node) {
|
|
||||||
if n, ok := n.(*ir.ReturnStmt); ok {
|
|
||||||
nreturns++
|
|
||||||
if len(n.Results) == 0 {
|
|
||||||
delayretvars = false // empty return statement (case 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if nreturns != 1 {
|
|
||||||
delayretvars = false // not exactly one return statement (case 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// temporaries for return values.
|
// temporaries for return values.
|
||||||
var retvars []ir.Node
|
var retvars []ir.Node
|
||||||
for i, t := range fn.Type().Results().Fields().Slice() {
|
for i, t := range fn.Type().Results().Fields().Slice() {
|
||||||
|
@ -885,7 +939,6 @@ func mkinlcall(n *ir.CallExpr, fn *ir.Func, maxCost int32, inlMap map[*ir.Func]b
|
||||||
m = inlvar(n)
|
m = inlvar(n)
|
||||||
m = typecheck.Expr(m).(*ir.Name)
|
m = typecheck.Expr(m).(*ir.Name)
|
||||||
inlvars[n] = m
|
inlvars[n] = m
|
||||||
delayretvars = false // found a named result parameter (case 3)
|
|
||||||
} else {
|
} else {
|
||||||
// anonymous return values, synthesize names for use in assignment that replaces return
|
// anonymous return values, synthesize names for use in assignment that replaces return
|
||||||
m = retvar(t, i)
|
m = retvar(t, i)
|
||||||
|
@ -908,14 +961,14 @@ func mkinlcall(n *ir.CallExpr, fn *ir.Func, maxCost int32, inlMap map[*ir.Func]b
|
||||||
// Assign arguments to the parameters' temp names.
|
// Assign arguments to the parameters' temp names.
|
||||||
as := ir.NewAssignListStmt(base.Pos, ir.OAS2, nil, nil)
|
as := ir.NewAssignListStmt(base.Pos, ir.OAS2, nil, nil)
|
||||||
as.Def = true
|
as.Def = true
|
||||||
if n.Op() == ir.OCALLMETH {
|
if call.Op() == ir.OCALLMETH {
|
||||||
sel := n.X.(*ir.SelectorExpr)
|
sel := call.X.(*ir.SelectorExpr)
|
||||||
if sel.X == nil {
|
if sel.X == nil {
|
||||||
base.Fatalf("method call without receiver: %+v", n)
|
base.Fatalf("method call without receiver: %+v", call)
|
||||||
}
|
}
|
||||||
as.Rhs.Append(sel.X)
|
as.Rhs.Append(sel.X)
|
||||||
}
|
}
|
||||||
as.Rhs.Append(n.Args...)
|
as.Rhs.Append(call.Args...)
|
||||||
|
|
||||||
if recv := fn.Type().Recv(); recv != nil {
|
if recv := fn.Type().Recv(); recv != nil {
|
||||||
as.Lhs.Append(inlParam(recv, as, inlvars))
|
as.Lhs.Append(inlParam(recv, as, inlvars))
|
||||||
|
@ -928,7 +981,7 @@ func mkinlcall(n *ir.CallExpr, fn *ir.Func, maxCost int32, inlMap map[*ir.Func]b
|
||||||
ninit.Append(typecheck.Stmt(as))
|
ninit.Append(typecheck.Stmt(as))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !delayretvars {
|
if !fn.Inl.CanDelayResults {
|
||||||
// Zero the return parameters.
|
// Zero the return parameters.
|
||||||
for _, n := range retvars {
|
for _, n := range retvars {
|
||||||
ninit.Append(ir.NewDecl(base.Pos, ir.ODCL, n.(*ir.Name)))
|
ninit.Append(ir.NewDecl(base.Pos, ir.ODCL, n.(*ir.Name)))
|
||||||
|
@ -941,40 +994,21 @@ func mkinlcall(n *ir.CallExpr, fn *ir.Func, maxCost int32, inlMap map[*ir.Func]b
|
||||||
|
|
||||||
inlgen++
|
inlgen++
|
||||||
|
|
||||||
parent := -1
|
|
||||||
if b := base.Ctxt.PosTable.Pos(n.Pos()).Base(); b != nil {
|
|
||||||
parent = b.InliningIndex()
|
|
||||||
}
|
|
||||||
|
|
||||||
sym := fn.Linksym()
|
|
||||||
newIndex := base.Ctxt.InlTree.Add(parent, n.Pos(), sym)
|
|
||||||
|
|
||||||
// Add an inline mark just before the inlined body.
|
// Add an inline mark just before the inlined body.
|
||||||
// This mark is inline in the code so that it's a reasonable spot
|
// This mark is inline in the code so that it's a reasonable spot
|
||||||
// to put a breakpoint. Not sure if that's really necessary or not
|
// to put a breakpoint. Not sure if that's really necessary or not
|
||||||
// (in which case it could go at the end of the function instead).
|
// (in which case it could go at the end of the function instead).
|
||||||
// Note issue 28603.
|
// Note issue 28603.
|
||||||
inlMark := ir.NewInlineMarkStmt(base.Pos, types.BADWIDTH)
|
ninit.Append(ir.NewInlineMarkStmt(call.Pos().WithIsStmt(), int64(inlIndex)))
|
||||||
inlMark.SetPos(n.Pos().WithIsStmt())
|
|
||||||
inlMark.Index = int64(newIndex)
|
|
||||||
ninit.Append(inlMark)
|
|
||||||
|
|
||||||
if base.Flag.GenDwarfInl > 0 {
|
|
||||||
if !sym.WasInlined() {
|
|
||||||
base.Ctxt.DwFixups.SetPrecursorFunc(sym, fn)
|
|
||||||
sym.Set(obj.AttrWasInlined, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
subst := inlsubst{
|
subst := inlsubst{
|
||||||
retlabel: retlabel,
|
retlabel: retlabel,
|
||||||
retvars: retvars,
|
retvars: retvars,
|
||||||
delayretvars: delayretvars,
|
inlvars: inlvars,
|
||||||
inlvars: inlvars,
|
defnMarker: ir.NilExpr{},
|
||||||
defnMarker: ir.NilExpr{},
|
bases: make(map[*src.PosBase]*src.PosBase),
|
||||||
bases: make(map[*src.PosBase]*src.PosBase),
|
newInlIndex: inlIndex,
|
||||||
newInlIndex: newIndex,
|
fn: fn,
|
||||||
fn: fn,
|
|
||||||
}
|
}
|
||||||
subst.edit = subst.node
|
subst.edit = subst.node
|
||||||
|
|
||||||
|
@ -995,26 +1029,11 @@ func mkinlcall(n *ir.CallExpr, fn *ir.Func, maxCost int32, inlMap map[*ir.Func]b
|
||||||
|
|
||||||
//dumplist("ninit post", ninit);
|
//dumplist("ninit post", ninit);
|
||||||
|
|
||||||
call := ir.NewInlinedCallExpr(base.Pos, nil, nil)
|
res := ir.NewInlinedCallExpr(base.Pos, body, retvars)
|
||||||
*call.PtrInit() = ninit
|
res.SetInit(ninit)
|
||||||
call.Body = body
|
res.SetType(call.Type())
|
||||||
call.ReturnVars = retvars
|
res.SetTypecheck(1)
|
||||||
call.SetType(n.Type())
|
return res
|
||||||
call.SetTypecheck(1)
|
|
||||||
|
|
||||||
// transitive inlining
|
|
||||||
// might be nice to do this before exporting the body,
|
|
||||||
// but can't emit the body with inlining expanded.
|
|
||||||
// instead we emit the things that the body needs
|
|
||||||
// and each use must redo the inlining.
|
|
||||||
// luckily these are small.
|
|
||||||
ir.EditChildren(call, edit)
|
|
||||||
|
|
||||||
if base.Flag.LowerM > 2 {
|
|
||||||
fmt.Printf("%v: After inlining %+v\n\n", ir.Line(call), call)
|
|
||||||
}
|
|
||||||
|
|
||||||
return call
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every time we expand a function we generate a new set of tmpnames,
|
// Every time we expand a function we generate a new set of tmpnames,
|
||||||
|
@ -1057,10 +1076,6 @@ type inlsubst struct {
|
||||||
// Temporary result variables.
|
// Temporary result variables.
|
||||||
retvars []ir.Node
|
retvars []ir.Node
|
||||||
|
|
||||||
// Whether result variables should be initialized at the
|
|
||||||
// "return" statement.
|
|
||||||
delayretvars bool
|
|
||||||
|
|
||||||
inlvars map[*ir.Name]*ir.Name
|
inlvars map[*ir.Name]*ir.Name
|
||||||
// defnMarker is used to mark a Node for reassignment.
|
// defnMarker is used to mark a Node for reassignment.
|
||||||
// inlsubst.clovar set this during creating new ONAME.
|
// inlsubst.clovar set this during creating new ONAME.
|
||||||
|
@ -1353,7 +1368,7 @@ func (subst *inlsubst) node(n ir.Node) ir.Node {
|
||||||
}
|
}
|
||||||
as.Rhs = subst.list(n.Results)
|
as.Rhs = subst.list(n.Results)
|
||||||
|
|
||||||
if subst.delayretvars {
|
if subst.fn.Inl.CanDelayResults {
|
||||||
for _, n := range as.Lhs {
|
for _, n := range as.Lhs {
|
||||||
as.PtrInit().Append(ir.NewDecl(base.Pos, ir.ODCL, n.(*ir.Name)))
|
as.PtrInit().Append(ir.NewDecl(base.Pos, ir.ODCL, n.(*ir.Name)))
|
||||||
n.Name().Defn = as
|
n.Name().Defn = as
|
||||||
|
|
|
@ -166,6 +166,11 @@ type Inline struct {
|
||||||
// another package is imported.
|
// another package is imported.
|
||||||
Dcl []*Name
|
Dcl []*Name
|
||||||
Body []Node
|
Body []Node
|
||||||
|
|
||||||
|
// CanDelayResults reports whether it's safe for the inliner to delay
|
||||||
|
// initializing the result parameters until immediately before the
|
||||||
|
// "return" statement.
|
||||||
|
CanDelayResults bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// A Mark represents a scope boundary.
|
// A Mark represents a scope boundary.
|
||||||
|
|
|
@ -1313,6 +1313,7 @@ func (w *exportWriter) funcExt(n *ir.Name) {
|
||||||
}
|
}
|
||||||
if n.Func.Inl != nil {
|
if n.Func.Inl != nil {
|
||||||
w.uint64(1 + uint64(n.Func.Inl.Cost))
|
w.uint64(1 + uint64(n.Func.Inl.Cost))
|
||||||
|
w.bool(n.Func.Inl.CanDelayResults)
|
||||||
if n.Func.ExportInline() || n.Type().HasTParam() {
|
if n.Func.ExportInline() || n.Type().HasTParam() {
|
||||||
w.p.doInline(n)
|
w.p.doInline(n)
|
||||||
}
|
}
|
||||||
|
|
|
@ -927,7 +927,8 @@ func (r *importReader) funcExt(n *ir.Name) {
|
||||||
// Inline body.
|
// Inline body.
|
||||||
if u := r.uint64(); u > 0 {
|
if u := r.uint64(); u > 0 {
|
||||||
n.Func.Inl = &ir.Inline{
|
n.Func.Inl = &ir.Inline{
|
||||||
Cost: int32(u - 1),
|
Cost: int32(u - 1),
|
||||||
|
CanDelayResults: r.bool(),
|
||||||
}
|
}
|
||||||
n.Func.Endlineno = r.pos()
|
n.Func.Endlineno = r.pos()
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,5 +21,5 @@ var x = 5
|
||||||
//go:noinline Provide a clean, constant reason for not inlining main
|
//go:noinline Provide a clean, constant reason for not inlining main
|
||||||
func main() { // ERROR "cannot inline main: marked go:noinline$"
|
func main() { // ERROR "cannot inline main: marked go:noinline$"
|
||||||
println("Foo(", x, ")=", Foo(x))
|
println("Foo(", x, ")=", Foo(x))
|
||||||
println("Bar(", x, ")=", Bar(x)) // ERROR "inlining call to Bar func\(int\) int { return x \* \(x \+ 1\) \* \(x \+ 2\) }$"
|
println("Bar(", x, ")=", Bar(x)) // ERROR "inlining call to Bar"
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,6 +19,6 @@ var x = 5
|
||||||
|
|
||||||
//go:noinline Provide a clean, constant reason for not inlining main
|
//go:noinline Provide a clean, constant reason for not inlining main
|
||||||
func main() { // ERROR "cannot inline main: marked go:noinline$"
|
func main() { // ERROR "cannot inline main: marked go:noinline$"
|
||||||
println("Foo(", x, ")=", Foo(x)) // ERROR "inlining call to Foo func\(int\) int { return x \* \(x \+ 1\) \* \(x \+ 2\) }$"
|
println("Foo(", x, ")=", Foo(x)) // ERROR "inlining call to Foo"
|
||||||
println("Bar(", x, ")=", Bar(x)) // ERROR "inlining call to Bar func\(int\) int { return x \* \(x \+ 1\) \* \(x \+ 2\) }$"
|
println("Bar(", x, ")=", Bar(x)) // ERROR "inlining call to Bar"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1023,7 +1023,7 @@ func f(a []int) int { // ERROR "cannot inline f:.*" "a does not escape"
|
||||||
a[997] = 0
|
a[997] = 0
|
||||||
a[998] = 0
|
a[998] = 0
|
||||||
a[999] = 0
|
a[999] = 0
|
||||||
x := small(a) // ERROR "inlining call to small .*"
|
x := small(a) // ERROR "inlining call to small"
|
||||||
y := medium(a) // The crux of this test: medium is not inlined.
|
y := medium(a) // The crux of this test: medium is not inlined.
|
||||||
return x + y
|
return x + y
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue