From 58c87329d4eeeab71149a8b445c69e0ee4e5db19 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sun, 2 Sep 2018 03:13:39 +0200 Subject: [PATCH] Implement closures and bound methods --- README.markdown | 3 +- compiler.go | 256 +++++++++++++++++++++++++++++++++--- ir.go | 22 ++-- passes.go | 110 +++++++++++++--- src/examples/hello/hello.go | 15 +++ 5 files changed, 357 insertions(+), 49 deletions(-) diff --git a/README.markdown b/README.markdown index 6d75ab04..56f2f372 100644 --- a/README.markdown +++ b/README.markdown @@ -48,13 +48,14 @@ Currently supported features: * slices (partially) * maps (very rough, unfinished) * defer (only in trivial cases) + * closures + * bound methods Not yet supported: * complex numbers * garbage collection * recover - * closures * channels * introspection (if it ever gets implemented) * ... diff --git a/compiler.go b/compiler.go index 4db2aed8..2ab485bf 100644 --- a/compiler.go +++ b/compiler.go @@ -214,6 +214,7 @@ func (c *Compiler) Parse(mainPath string, buildTags []string) error { c.ir.SimpleDCE() // remove most dead code c.ir.AnalyseCallgraph() // set up callgraph c.ir.AnalyseInterfaceConversions() // determine which types are converted to an interface + c.ir.AnalyseFunctionPointers() // determine which function pointer signatures need context c.ir.AnalyseBlockingRecursive() // make all parents of blocking calls blocking (transitively) c.ir.AnalyseGoCalls() // check whether we need a scheduler @@ -542,8 +543,18 @@ func (c *Compiler) getLLVMType(goType types.Type) (llvm.Type, error) { } paramTypes = append(paramTypes, subType) } - // make a function pointer of it - return llvm.PointerType(llvm.FunctionType(returnType, paramTypes, false), 0), nil + var ptr llvm.Type + if c.ir.SignatureNeedsContext(typ) { + // make a closure type (with a function pointer type inside): + // {context, funcptr} + paramTypes = append(paramTypes, c.i8ptrType) + ptr = llvm.PointerType(llvm.FunctionType(returnType, paramTypes, false), 0) + ptr = c.ctx.StructType([]llvm.Type{c.i8ptrType, ptr}, false) + } else { + // make a simple function pointer + ptr = llvm.PointerType(llvm.FunctionType(returnType, paramTypes, false), 0) + } + return ptr, nil case *types.Slice: elemType, err := c.getLLVMType(typ.Elem()) if err != nil { @@ -703,6 +714,12 @@ func (c *Compiler) parseFuncDecl(f *Function) (*Frame, error) { frame.params[param] = i } + if c.ir.FunctionNeedsContext(f) { + // This function gets an extra parameter: the context pointer (for + // closures and bound methods). Add it as an extra paramter here. + paramTypes = append(paramTypes, c.i8ptrType) + } + fnType := llvm.FunctionType(retType, paramTypes, false) name := f.LinkName() @@ -781,7 +798,13 @@ func (c *Compiler) getInterpretedValue(value Value) (llvm.Value, error) { } return getZeroValue(llvmType) } - return c.ir.GetFunction(value.Elem).llvmFn, nil + fn := c.ir.GetFunction(value.Elem) + ptr := fn.llvmFn + if c.ir.SignatureNeedsContext(fn.fn.Signature) { + // Create closure value: {context, function pointer} + ptr = llvm.ConstStruct([]llvm.Value{llvm.ConstPointerNull(c.i8ptrType), ptr}, false) + } + return ptr, nil case *GlobalValue: zero := llvm.ConstInt(llvm.Int32Type(), 0, false) @@ -1001,6 +1024,54 @@ func (c *Compiler) parseFunc(frame *Frame) error { frame.locals[param] = llvmParam } + // Load free variables from the context. This is a closure (or bound + // method). + if len(frame.fn.fn.FreeVars) != 0 { + if !c.ir.FunctionNeedsContext(frame.fn) { + panic("free variables on function without context") + } + c.builder.SetInsertPointAtEnd(frame.blocks[frame.fn.fn.Blocks[0]]) + context := frame.fn.llvmFn.Param(len(frame.fn.fn.Params)) + + // Determine the context type. It's a struct containing all variables. + freeVarTypes := make([]llvm.Type, 0, len(frame.fn.fn.FreeVars)) + for _, freeVar := range frame.fn.fn.FreeVars { + typ, err := c.getLLVMType(freeVar.Type()) + if err != nil { + return err + } + freeVarTypes = append(freeVarTypes, typ) + } + contextType := llvm.StructType(freeVarTypes, false) + + // Get a correctly-typed pointer to the context. + contextAlloc := llvm.Value{} + if c.targetData.TypeAllocSize(contextType) <= c.targetData.TypeAllocSize(c.i8ptrType) { + // Context stored directly in pointer. Load it using an alloca. + contextRawAlloc := c.builder.CreateAlloca(llvm.PointerType(c.i8ptrType, 0), "") + contextRawValue := c.builder.CreateBitCast(context, llvm.PointerType(c.i8ptrType, 0), "") + c.builder.CreateStore(contextRawValue, contextRawAlloc) + contextAlloc = c.builder.CreateBitCast(contextRawAlloc, llvm.PointerType(contextType, 0), "") + } else { + // Context stored in the heap. Bitcast the passed-in pointer to the + // correct pointer type. + contextAlloc = c.builder.CreateBitCast(context, llvm.PointerType(contextType, 0), "") + } + + // Load each free variable from the context. + // A free variable is always a pointer when this is a closure, but it + // can be another type when it is a wrapper for a bound method (these + // wrappers are generated by the ssa package). + for i, freeVar := range frame.fn.fn.FreeVars { + indices := []llvm.Value{ + llvm.ConstInt(llvm.Int32Type(), 0, false), + llvm.ConstInt(llvm.Int32Type(), uint64(i), false), + } + gep := c.builder.CreateInBoundsGEP(contextAlloc, indices, "") + frame.locals[freeVar] = c.builder.CreateLoad(gep, "") + } + } + if frame.blocking { // Coroutine initialization. c.builder.SetInsertPointAtEnd(frame.blocks[frame.fn.fn.Blocks[0]]) @@ -1372,7 +1443,7 @@ func (c *Compiler) parseBuiltin(frame *Frame, args []ssa.Value, callName string) } } -func (c *Compiler) parseFunctionCall(frame *Frame, args []ssa.Value, llvmFn llvm.Value, blocking bool, parentHandle llvm.Value) (llvm.Value, error) { +func (c *Compiler) parseFunctionCall(frame *Frame, args []ssa.Value, llvmFn, context llvm.Value, blocking bool, parentHandle llvm.Value) (llvm.Value, error) { var params []llvm.Value if blocking { if parentHandle.IsNil() { @@ -1391,6 +1462,12 @@ func (c *Compiler) parseFunctionCall(frame *Frame, args []ssa.Value, llvmFn llvm params = append(params, val) } + if !context.IsNil() { + // This function takes a context parameter. + // Add it to the end of the parameter list. + params = append(params, context) + } + if frame.blocking && llvmFn.Name() == "runtime.Sleep" { // Set task state to TASK_STATE_SLEEP and set the duration. c.builder.CreateCall(c.mod.NamedFunction("runtime.sleepTask"), []llvm.Value{frame.taskHandle, params[0]}, "") @@ -1443,10 +1520,22 @@ func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon, parentHandle l if err != nil { return llvm.Value{}, err } + llvmFnType, err := c.getLLVMType(instr.Method.Type()) if err != nil { return llvm.Value{}, err } + if c.ir.SignatureNeedsContext(instr.Method.Type().(*types.Signature)) { + // This is somewhat of a hack. + // getLLVMType() has created a closure type for us, but we don't + // actually want a closure type as an interface call can never be a + // closure call. So extract the function pointer type from the + // closure. + // This happens because somewhere the same function signature is + // used in a closure or bound method. + llvmFnType = llvmFnType.Subtypes()[1] + } + values := []llvm.Value{ itf, llvm.ConstInt(llvm.Int16Type(), uint64(c.ir.MethodNum(instr.Method)), false), @@ -1454,6 +1543,7 @@ func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon, parentHandle l fn := c.builder.CreateCall(c.mod.NamedFunction("runtime.interfaceMethod"), values, "invoke.func") fnCast := c.builder.CreateBitCast(fn, llvmFnType, "invoke.func.cast") receiverValue := c.builder.CreateExtractValue(itf, 1, "invoke.func.receiver") + args := []llvm.Value{receiverValue} for _, arg := range instr.Args { val, err := c.parseExpr(frame, arg) @@ -1462,16 +1552,21 @@ func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon, parentHandle l } args = append(args, val) } + if c.ir.SignatureNeedsContext(instr.Method.Type().(*types.Signature)) { + // This function takes an extra context parameter. An interface call + // cannot also be a closure but we have to supply the nil pointer + // anyway. + args = append(args, llvm.ConstPointerNull(c.i8ptrType)) + } + // TODO: blocking methods (needs analysis) return c.builder.CreateCall(fnCast, args, ""), nil } - // Regular function, builtin, or function pointer. - switch call := instr.Value.(type) { - case *ssa.Builtin: - return c.parseBuiltin(frame, instr.Args, call.Name()) - case *ssa.Function: - if call.Name() == "Asm" && len(instr.Args) == 1 { + // Try to call the function directly for trivially static calls. + fn := instr.StaticCallee() + if fn != nil { + if fn.Name() == "Asm" && len(instr.Args) == 1 { // Magic function: insert inline assembly instead of calling it. if named, ok := instr.Args[0].Type().(*types.Named); ok && named.Obj().Name() == "__asm" { fnType := llvm.FunctionType(llvm.VoidType(), []llvm.Type{}, false) @@ -1480,20 +1575,52 @@ func (c *Compiler) parseCall(frame *Frame, instr *ssa.CallCommon, parentHandle l return c.builder.CreateCall(target, nil, ""), nil } } - targetFunc := c.ir.GetFunction(call) - name := targetFunc.LinkName() - llvmFn := c.mod.NamedFunction(name) - if llvmFn.IsNil() { - return llvm.Value{}, errors.New("undefined function: " + name) + targetFunc := c.ir.GetFunction(fn) + if targetFunc.llvmFn.IsNil() { + return llvm.Value{}, errors.New("undefined function: " + targetFunc.LinkName()) } - return c.parseFunctionCall(frame, instr.Args, llvmFn, targetFunc.blocking, parentHandle) + var context llvm.Value + if c.ir.FunctionNeedsContext(targetFunc) { + // This function call is to a (potential) closure, not a regular + // function. See whether it is a closure and if so, call it as such. + // Else, supply a dummy nil pointer as the last parameter. + var err error + if mkClosure, ok := instr.Value.(*ssa.MakeClosure); ok { + // closure is {context, function pointer} + closure, err := c.parseExpr(frame, mkClosure) + if err != nil { + return llvm.Value{}, err + } + context = c.builder.CreateExtractValue(closure, 0, "") + } else { + context, err = getZeroValue(c.i8ptrType) + if err != nil { + return llvm.Value{}, err + } + } + } + return c.parseFunctionCall(frame, instr.Args, targetFunc.llvmFn, context, targetFunc.blocking, parentHandle) + } + + // Builtin or function pointer. + switch call := instr.Value.(type) { + case *ssa.Builtin: + return c.parseBuiltin(frame, instr.Args, call.Name()) default: // function pointer value, err := c.parseExpr(frame, instr.Value) if err != nil { return llvm.Value{}, err } // TODO: blocking function pointers (needs analysis) - return c.parseFunctionCall(frame, instr.Args, value, false, parentHandle) + var context llvm.Value + if c.ir.SignatureNeedsContext(instr.Signature()) { + // 'value' is a closure, not a raw function pointer. + // Extract the function pointer and the context pointer. + // closure: {context, function pointer} + context = c.builder.CreateExtractValue(value, 0, "") + value = c.builder.CreateExtractValue(value, 1, "") + } + return c.parseFunctionCall(frame, instr.Args, value, context, false, parentHandle) } } @@ -1583,7 +1710,17 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) { } return c.builder.CreateGEP(val, indices, ""), nil case *ssa.Function: - return c.mod.NamedFunction(c.ir.GetFunction(expr).LinkName()), nil + fn := c.ir.GetFunction(expr) + ptr := fn.llvmFn + if c.ir.FunctionNeedsContext(fn) { + // Create closure for function pointer. + // Closure is: {context, function pointer} + ptr = llvm.ConstStruct([]llvm.Value{ + llvm.ConstPointerNull(c.i8ptrType), + ptr, + }, false) + } + return ptr, nil case *ssa.Global: if strings.HasPrefix(expr.Name(), "__cgofn__cgo_") || strings.HasPrefix(expr.Name(), "_cgo_") { // Ignore CGo global variables which we don't use. @@ -1731,6 +1868,10 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) { default: panic("unknown lookup type: " + expr.String()) } + + case *ssa.MakeClosure: + return c.parseMakeClosure(frame, expr) + case *ssa.MakeInterface: val, err := c.parseExpr(frame, expr.X) if err != nil { @@ -2249,6 +2390,85 @@ func (c *Compiler) parseConvert(typeFrom, typeTo types.Type, value llvm.Value) ( } } +func (c *Compiler) parseMakeClosure(frame *Frame, expr *ssa.MakeClosure) (llvm.Value, error) { + if len(expr.Bindings) == 0 { + panic("unexpected: MakeClosure without bound variables") + } + f := c.ir.GetFunction(expr.Fn.(*ssa.Function)) + if !c.ir.FunctionNeedsContext(f) { + // Maybe AnalyseFunctionPointers didn't run? + panic("MakeClosure on function signature without context") + } + + // Collect all bound variables. + boundVars := make([]llvm.Value, 0, len(expr.Bindings)) + boundVarTypes := make([]llvm.Type, 0, len(expr.Bindings)) + for _, binding := range expr.Bindings { + // The context stores the bound variables. + llvmBoundVar, err := c.parseExpr(frame, binding) + if err != nil { + return llvm.Value{}, err + } + boundVars = append(boundVars, llvmBoundVar) + boundVarTypes = append(boundVarTypes, llvmBoundVar.Type()) + } + contextType := llvm.StructType(boundVarTypes, false) + + // Allocate memory for the context. + contextAlloc := llvm.Value{} + contextHeapAlloc := llvm.Value{} + if c.targetData.TypeAllocSize(contextType) <= c.targetData.TypeAllocSize(c.i8ptrType) { + // Context fits in a pointer - e.g. when it is a pointer. Store it + // directly in the stack after a convert. + // Because contextType is a struct and we have to cast it to a *i8, + // store it in an alloca first for bitcasting (store+bitcast+load). + contextAlloc = c.builder.CreateAlloca(contextType, "") + } else { + // Context is bigger than a pointer, so allocate it on the heap. + size := c.targetData.TypeAllocSize(contextType) + sizeValue := llvm.ConstInt(c.uintptrType, size, false) + contextHeapAlloc = c.builder.CreateCall(c.allocFunc, []llvm.Value{sizeValue}, "") + contextAlloc = c.builder.CreateBitCast(contextHeapAlloc, llvm.PointerType(contextType, 0), "") + } + + // Store all bound variables in the alloca or heap pointer. + for i, boundVar := range boundVars { + indices := []llvm.Value{ + llvm.ConstInt(llvm.Int32Type(), 0, false), + llvm.ConstInt(llvm.Int32Type(), uint64(i), false), + } + gep := c.builder.CreateInBoundsGEP(contextAlloc, indices, "") + c.builder.CreateStore(boundVar, gep) + } + + context := llvm.Value{} + if c.targetData.TypeAllocSize(contextType) <= c.targetData.TypeAllocSize(c.i8ptrType) { + // Load value (as *i8) from the alloca. + contextAlloc = c.builder.CreateBitCast(contextAlloc, llvm.PointerType(c.i8ptrType, 0), "") + context = c.builder.CreateLoad(contextAlloc, "") + } else { + // Get the original heap allocation pointer, which already is an + // *i8. + context = contextHeapAlloc + } + + // Get the function signature type, which is a closure type. + // A closure is a tuple of {context, function pointer}. + typ, err := c.getLLVMType(f.fn.Signature) + if err != nil { + return llvm.Value{}, err + } + + // Create the closure, which is a struct: {context, function pointer}. + closure, err := getZeroValue(typ) + if err != nil { + return llvm.Value{}, err + } + closure = c.builder.CreateInsertValue(closure, f.llvmFn, 1, "") + closure = c.builder.CreateInsertValue(closure, context, 0, "") + return closure, nil +} + func (c *Compiler) parseMakeInterface(val llvm.Value, typ types.Type, isConst bool) (llvm.Value, error) { var itfValue llvm.Value size := c.targetData.TypeAllocSize(val.Type()) diff --git a/ir.go b/ir.go index f414ad37..69737be0 100644 --- a/ir.go +++ b/ir.go @@ -25,20 +25,22 @@ type Program struct { NamedTypes []*NamedType needsScheduler bool goCalls []*ssa.Go - typesWithMethods map[string]*InterfaceType - typesWithoutMethods map[string]int + typesWithMethods map[string]*InterfaceType // see AnalyseInterfaceConversions + typesWithoutMethods map[string]int // see AnalyseInterfaceConversions methodSignatureNames map[string]int + fpWithContext map[string]struct{} // see AnalyseFunctionPointers } // Function or method. type Function struct { - fn *ssa.Function - llvmFn llvm.Value - linkName string - blocking bool - flag bool // used by dead code elimination - parents []*Function // calculated by AnalyseCallgraph - children []*Function + fn *ssa.Function + llvmFn llvm.Value + linkName string + blocking bool + flag bool // used by dead code elimination + addressTaken bool // used as function pointer, calculated by AnalyseFunctionPointers + parents []*Function // calculated by AnalyseCallgraph + children []*Function // calculated by AnalyseCallgraph } // Global variable, possibly constant. @@ -117,9 +119,9 @@ func (p *Program) AddPackage(pkg *ssa.Package) { func (p *Program) addFunction(ssaFn *ssa.Function) { f := &Function{fn: ssaFn} + f.parsePragmas() p.Functions = append(p.Functions, f) p.functionMap[ssaFn] = f - f.parsePragmas() for _, anon := range ssaFn.AnonFuncs { p.addFunction(anon) diff --git a/passes.go b/passes.go index f0669696..2b2535b3 100644 --- a/passes.go +++ b/passes.go @@ -5,46 +5,55 @@ import ( "golang.org/x/tools/go/ssa" ) -// This function implements several optimization passes (analysis + transform) -// to optimize code in SSA form before it is compiled to LLVM IR. It is based on +// This file implements several optimization passes (analysis + transform) to +// optimize code in SSA form before it is compiled to LLVM IR. It is based on // the IR defined in ir.go. -// Make a readable version of the method signature (including the function name, +// Make a readable version of a method signature (including the function name, // excluding the receiver name). This string is used internally to match // interfaces and to call the correct method on an interface. Examples: // // String() string // Read([]byte) (int, error) -func MethodName(method *types.Func) string { - sig := method.Type().(*types.Signature) - name := method.Name() +func MethodSignature(method *types.Func) string { + return method.Name() + Signature(method.Type().(*types.Signature)) +} + +// Make a readable version of a function (pointer) signature. This string is +// used internally to match signatures (like in AnalyseFunctionPointers). +// Examples: +// +// () string +// (string, int) (int, error) +func Signature(sig *types.Signature) string { + s := "" if sig.Params().Len() == 0 { - name += "()" + s += "()" } else { - name += "(" + s += "(" for i := 0; i < sig.Params().Len(); i++ { if i > 0 { - name += ", " + s += ", " } - name += sig.Params().At(i).Type().String() + s += sig.Params().At(i).Type().String() } - name += ")" + s += ")" } if sig.Results().Len() == 0 { // keep as-is } else if sig.Results().Len() == 1 { - name += " " + sig.Results().At(0).Type().String() + s += " " + sig.Results().At(0).Type().String() } else { - name += " (" + s += " (" for i := 0; i < sig.Results().Len(); i++ { if i > 0 { - name += ", " + s += ", " } - name += sig.Results().At(i).Type().String() + s += sig.Results().At(i).Type().String() } - name += ")" + s += ")" } - return name + return s } // Fill in parents of all functions. @@ -111,7 +120,7 @@ func (p *Program) AnalyseInterfaceConversions() { Methods: make(map[string]*types.Selection), } for _, sel := range methods { - name := MethodName(sel.Obj().(*types.Func)) + name := MethodSignature(sel.Obj().(*types.Func)) t.Methods[name] = sel } p.typesWithMethods[name] = t @@ -124,6 +133,49 @@ func (p *Program) AnalyseInterfaceConversions() { } } +// Analyse which function pointer signatures need a context parameter. +// This makes calling function pointers more efficient. +func (p *Program) AnalyseFunctionPointers() { + // Clear, if AnalyseFunctionPointers has been called before. + p.fpWithContext = map[string]struct{}{} + + for _, f := range p.Functions { + for _, block := range f.fn.Blocks { + for _, instr := range block.Instrs { + switch instr := instr.(type) { + case ssa.CallInstruction: + for _, arg := range instr.Common().Args { + switch arg := arg.(type) { + case *ssa.Function: + f := p.GetFunction(arg) + f.addressTaken = true + } + } + case *ssa.DebugRef: + default: + // For anything that isn't a call... + for _, operand := range instr.Operands(nil) { + if operand == nil || *operand == nil || isCGoInternal((*operand).Name()) { + continue + } + switch operand := (*operand).(type) { + case *ssa.Function: + f := p.GetFunction(operand) + f.addressTaken = true + } + } + } + switch instr := instr.(type) { + case *ssa.MakeClosure: + fn := instr.Fn.(*ssa.Function) + sig := Signature(fn.Signature) + p.fpWithContext[sig] = struct{}{} + } + } + } + } +} + // Analyse which functions are recursively blocking. // // Depends on AnalyseCallgraph. @@ -233,6 +285,12 @@ func (p *Program) SimpleDCE() { switch operand := (*operand).(type) { case *ssa.Function: f := p.GetFunction(operand) + if f == nil { + // FIXME HACK: this function should have been + // discovered already. It is not for bound methods. + p.addFunction(operand) + f = p.GetFunction(operand) + } if !f.flag { f.flag = true worklist = append(worklist, operand) @@ -306,11 +364,11 @@ func (p *Program) TypeNum(typ types.Type) (int, bool) { // MethodNum returns the numeric ID of this method, to be used in method lookups // on interfaces for example. func (p *Program) MethodNum(method *types.Func) int { - name := MethodName(method) + name := MethodSignature(method) if _, ok := p.methodSignatureNames[name]; !ok { p.methodSignatureNames[name] = len(p.methodSignatureNames) } - return p.methodSignatureNames[MethodName(method)] + return p.methodSignatureNames[MethodSignature(method)] } // The start index of the first dynamic type that has methods. @@ -330,3 +388,15 @@ func (p *Program) AllDynamicTypes() []*InterfaceType { } return l } + +func (p *Program) FunctionNeedsContext(f *Function) bool { + if !f.addressTaken { + return false + } + return p.SignatureNeedsContext(f.fn.Signature) +} + +func (p *Program) SignatureNeedsContext(sig *types.Signature) bool { + _, needsContext := p.fpWithContext[Signature(sig)] + return needsContext +} diff --git a/src/examples/hello/hello.go b/src/examples/hello/hello.go index 3362dd7f..1fa8886c 100644 --- a/src/examples/hello/hello.go +++ b/src/examples/hello/hello.go @@ -27,15 +27,18 @@ func main() { println("sumrange(100) =", sumrange(100)) println("strlen foo:", strlen("foo")) + // map m := map[string]int{"answer": 42, "foo": 3} readMap(m, "answer") readMap(testmap, "data") + // slice foo := []int{1, 2, 4, 5} println("len/cap foo:", len(foo), cap(foo)) println("foo[3]:", foo[3]) println("sum foo:", sum(foo)) + // interfaces, pointers thing := &Thing{"foo"} println("thing:", thing.String()) printItf(5) @@ -47,8 +50,16 @@ func main() { s := Stringer(thing) println("Stringer.String():", s.String()) + // unusual calls runFunc(hello, 5) // must be indirect to avoid obvious inlining testDefer() + testBound(thing.String) + func() { + println("thing inside closure:", thing.String()) //, len(foo)) + }() + runFunc(func(i int) { + println("inside fp closure:", thing.String(), i) + }, 3) // test library functions println("lower to upper char:", 'h', "->", unicode.ToUpper('h')) @@ -70,6 +81,10 @@ func deferred(msg string, i int) { println(msg, i) } +func testBound(f func() string) { + println("bound method:", f()) +} + func readMap(m map[string]int, key string) { println("map length:", len(m)) println("map read:", key, "=", m[key])