internal/task: remove coroutines
Этот коммит содержится в:
родитель
d054d4d512
коммит
ea2a6b70b2
21 изменённых файлов: 41 добавлений и 1962 удалений
|
@ -114,7 +114,7 @@ func (c *Config) NeedsStackObjects() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Scheduler returns the scheduler implementation. Valid values are "none",
|
// Scheduler returns the scheduler implementation. Valid values are "none",
|
||||||
//"coroutines" and "tasks".
|
// "asyncify" and "tasks".
|
||||||
func (c *Config) Scheduler() string {
|
func (c *Config) Scheduler() string {
|
||||||
if c.Options.Scheduler != "" {
|
if c.Options.Scheduler != "" {
|
||||||
return c.Options.Scheduler
|
return c.Options.Scheduler
|
||||||
|
@ -122,8 +122,8 @@ func (c *Config) Scheduler() string {
|
||||||
if c.Target.Scheduler != "" {
|
if c.Target.Scheduler != "" {
|
||||||
return c.Target.Scheduler
|
return c.Target.Scheduler
|
||||||
}
|
}
|
||||||
// Fall back to coroutines, which are supported everywhere.
|
// Fall back to none.
|
||||||
return "coroutines"
|
return "none"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serial returns the serial implementation for this build configuration: uart,
|
// Serial returns the serial implementation for this build configuration: uart,
|
||||||
|
@ -171,13 +171,10 @@ func (c *Config) FuncImplementation() string {
|
||||||
// being pointed to doesn't need a context. The function pointer is a
|
// being pointed to doesn't need a context. The function pointer is a
|
||||||
// regular function pointer.
|
// regular function pointer.
|
||||||
return "doubleword"
|
return "doubleword"
|
||||||
case "none", "coroutines":
|
case "none":
|
||||||
// As "doubleword", but with the function pointer replaced by a unique
|
// As "doubleword", but with the function pointer replaced by a unique
|
||||||
// ID per function signature. Function values are called by using a
|
// ID per function signature. Function values are called by using a
|
||||||
// switch statement and choosing which function to call.
|
// switch statement and choosing which function to call.
|
||||||
// Pick the switch implementation with the coroutines scheduler, as it
|
|
||||||
// allows the use of blocking inside a function that is used as a func
|
|
||||||
// value.
|
|
||||||
return "switch"
|
return "switch"
|
||||||
default:
|
default:
|
||||||
panic("unknown scheduler type")
|
panic("unknown scheduler type")
|
||||||
|
|
|
@ -8,7 +8,7 @@ import (
|
||||||
|
|
||||||
var (
|
var (
|
||||||
validGCOptions = []string{"none", "leaking", "conservative"}
|
validGCOptions = []string{"none", "leaking", "conservative"}
|
||||||
validSchedulerOptions = []string{"none", "tasks", "coroutines", "asyncify"}
|
validSchedulerOptions = []string{"none", "tasks", "asyncify"}
|
||||||
validSerialOptions = []string{"none", "uart", "usb"}
|
validSerialOptions = []string{"none", "uart", "usb"}
|
||||||
validPrintSizeOptions = []string{"none", "short", "full"}
|
validPrintSizeOptions = []string{"none", "short", "full"}
|
||||||
validPanicStrategyOptions = []string{"print", "trap"}
|
validPanicStrategyOptions = []string{"print", "trap"}
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
func TestVerifyOptions(t *testing.T) {
|
func TestVerifyOptions(t *testing.T) {
|
||||||
|
|
||||||
expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative`)
|
expectedGCError := errors.New(`invalid gc option 'incorrect': valid values are none, leaking, conservative`)
|
||||||
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, coroutines, asyncify`)
|
expectedSchedulerError := errors.New(`invalid scheduler option 'incorrect': valid values are none, tasks, asyncify`)
|
||||||
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full`)
|
expectedPrintSizeError := errors.New(`invalid size option 'incorrect': valid values are none, short, full`)
|
||||||
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
|
expectedPanicStrategyError := errors.New(`invalid panic option 'incorrect': valid values are print, trap`)
|
||||||
|
|
||||||
|
@ -67,12 +67,6 @@ func TestVerifyOptions(t *testing.T) {
|
||||||
Scheduler: "tasks",
|
Scheduler: "tasks",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "SchedulerOptionCoroutines",
|
|
||||||
opts: compileopts.Options{
|
|
||||||
Scheduler: "coroutines",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "InvalidPrintSizeOption",
|
name: "InvalidPrintSizeOption",
|
||||||
opts: compileopts.Options{
|
opts: compileopts.Options{
|
||||||
|
|
|
@ -55,10 +55,9 @@ func TestCompiler(t *testing.T) {
|
||||||
{"string.go", "", ""},
|
{"string.go", "", ""},
|
||||||
{"float.go", "", ""},
|
{"float.go", "", ""},
|
||||||
{"interface.go", "", ""},
|
{"interface.go", "", ""},
|
||||||
{"func.go", "", "coroutines"},
|
{"func.go", "", "none"},
|
||||||
{"pragma.go", "", ""},
|
{"pragma.go", "", ""},
|
||||||
{"goroutine.go", "wasm", "asyncify"},
|
{"goroutine.go", "wasm", "asyncify"},
|
||||||
{"goroutine.go", "wasm", "coroutines"},
|
|
||||||
{"goroutine.go", "cortex-m-qemu", "tasks"},
|
{"goroutine.go", "cortex-m-qemu", "tasks"},
|
||||||
{"channel.go", "", ""},
|
{"channel.go", "", ""},
|
||||||
{"intrinsics.go", "cortex-m-qemu", ""},
|
{"intrinsics.go", "cortex-m-qemu", ""},
|
||||||
|
|
|
@ -30,11 +30,6 @@ func (b *builder) createGo(instr *ssa.Go) {
|
||||||
switch value := instr.Call.Value.(type) {
|
switch value := instr.Call.Value.(type) {
|
||||||
case *ssa.Function:
|
case *ssa.Function:
|
||||||
// Goroutine call is regular function call. No context is necessary.
|
// Goroutine call is regular function call. No context is necessary.
|
||||||
if b.Scheduler == "coroutines" {
|
|
||||||
// The context parameter is assumed to be always present in the
|
|
||||||
// coroutines scheduler.
|
|
||||||
context = llvm.Undef(b.i8ptrType)
|
|
||||||
}
|
|
||||||
case *ssa.MakeClosure:
|
case *ssa.MakeClosure:
|
||||||
// A goroutine call on a func value, but the callee is trivial to find. For
|
// A goroutine call on a func value, but the callee is trivial to find. For
|
||||||
// example: immediately applied functions.
|
// example: immediately applied functions.
|
||||||
|
@ -98,7 +93,7 @@ func (b *builder) createGo(instr *ssa.Go) {
|
||||||
params = append(params, context) // context parameter
|
params = append(params, context) // context parameter
|
||||||
hasContext = true
|
hasContext = true
|
||||||
switch b.Scheduler {
|
switch b.Scheduler {
|
||||||
case "none", "coroutines":
|
case "none":
|
||||||
// There are no additional parameters needed for the goroutine start operation.
|
// There are no additional parameters needed for the goroutine start operation.
|
||||||
case "tasks", "asyncify":
|
case "tasks", "asyncify":
|
||||||
// Add the function pointer as a parameter to start the goroutine.
|
// Add the function pointer as a parameter to start the goroutine.
|
||||||
|
@ -110,32 +105,22 @@ func (b *builder) createGo(instr *ssa.Go) {
|
||||||
}
|
}
|
||||||
|
|
||||||
paramBundle := b.emitPointerPack(params)
|
paramBundle := b.emitPointerPack(params)
|
||||||
var callee, stackSize llvm.Value
|
var stackSize llvm.Value
|
||||||
switch b.Scheduler {
|
callee := b.createGoroutineStartWrapper(funcPtr, prefix, hasContext, instr.Pos())
|
||||||
case "none", "tasks", "asyncify":
|
if b.AutomaticStackSize {
|
||||||
callee = b.createGoroutineStartWrapper(funcPtr, prefix, hasContext, instr.Pos())
|
// The stack size is not known until after linking. Call a dummy
|
||||||
if b.AutomaticStackSize {
|
// function that will be replaced with a load from a special ELF
|
||||||
// The stack size is not known until after linking. Call a dummy
|
// section that contains the stack size (and is modified after
|
||||||
// function that will be replaced with a load from a special ELF
|
// linking).
|
||||||
// section that contains the stack size (and is modified after
|
stackSizeFn := b.getFunction(b.program.ImportedPackage("internal/task").Members["getGoroutineStackSize"].(*ssa.Function))
|
||||||
// linking).
|
stackSize = b.createCall(stackSizeFn, []llvm.Value{callee, llvm.Undef(b.i8ptrType), llvm.Undef(b.i8ptrType)}, "stacksize")
|
||||||
stackSizeFn := b.getFunction(b.program.ImportedPackage("internal/task").Members["getGoroutineStackSize"].(*ssa.Function))
|
} else {
|
||||||
stackSize = b.createCall(stackSizeFn, []llvm.Value{callee, llvm.Undef(b.i8ptrType), llvm.Undef(b.i8ptrType)}, "stacksize")
|
// The stack size is fixed at compile time. By emitting it here as a
|
||||||
} else {
|
// constant, it can be optimized.
|
||||||
// The stack size is fixed at compile time. By emitting it here as a
|
if (b.Scheduler == "tasks" || b.Scheduler == "asyncify") && b.DefaultStackSize == 0 {
|
||||||
// constant, it can be optimized.
|
b.addError(instr.Pos(), "default stack size for goroutines is not set")
|
||||||
if (b.Scheduler == "tasks" || b.Scheduler == "asyncify") && b.DefaultStackSize == 0 {
|
|
||||||
b.addError(instr.Pos(), "default stack size for goroutines is not set")
|
|
||||||
}
|
|
||||||
stackSize = llvm.ConstInt(b.uintptrType, b.DefaultStackSize, false)
|
|
||||||
}
|
}
|
||||||
case "coroutines":
|
stackSize = llvm.ConstInt(b.uintptrType, b.DefaultStackSize, false)
|
||||||
callee = b.CreatePtrToInt(funcPtr, b.uintptrType, "")
|
|
||||||
// There is no goroutine stack size: coroutines are used instead of
|
|
||||||
// stacks.
|
|
||||||
stackSize = llvm.Undef(b.uintptrType)
|
|
||||||
default:
|
|
||||||
panic("unreachable")
|
|
||||||
}
|
}
|
||||||
start := b.getFunction(b.program.ImportedPackage("internal/task").Members["start"].(*ssa.Function))
|
start := b.getFunction(b.program.ImportedPackage("internal/task").Members["start"].(*ssa.Function))
|
||||||
b.createCall(start, []llvm.Value{callee, paramBundle, stackSize, llvm.Undef(b.i8ptrType), llvm.ConstPointerNull(b.i8ptrType)}, "")
|
b.createCall(start, []llvm.Value{callee, paramBundle, stackSize, llvm.Undef(b.i8ptrType), llvm.ConstPointerNull(b.i8ptrType)}, "")
|
||||||
|
@ -297,8 +282,8 @@ func (c *compilerContext) createGoroutineStartWrapper(fn llvm.Value, prefix stri
|
||||||
// The last parameter in the packed object has somewhat of a dual role.
|
// The last parameter in the packed object has somewhat of a dual role.
|
||||||
// Inside the parameter bundle it's the function pointer, stored right
|
// Inside the parameter bundle it's the function pointer, stored right
|
||||||
// after the context pointer. But in the IR call instruction, it's the
|
// after the context pointer. But in the IR call instruction, it's the
|
||||||
// parentHandle function that's always undef outside of the coroutines
|
// parentHandle function that's always undef. Thus, make the parameter
|
||||||
// scheduler. Thus, make the parameter undef here.
|
// undef here.
|
||||||
params[len(params)-1] = llvm.Undef(c.i8ptrType)
|
params[len(params)-1] = llvm.Undef(c.i8ptrType)
|
||||||
|
|
||||||
// Create the call.
|
// Create the call.
|
||||||
|
|
0
compiler/testdata/func-coroutines.ll → compiler/testdata/func-none.ll
предоставленный
0
compiler/testdata/func-coroutines.ll → compiler/testdata/func-none.ll
предоставленный
149
compiler/testdata/goroutine-wasm-coroutines.ll
предоставленный
149
compiler/testdata/goroutine-wasm-coroutines.ll
предоставленный
|
@ -1,149 +0,0 @@
|
||||||
; ModuleID = 'goroutine.go'
|
|
||||||
source_filename = "goroutine.go"
|
|
||||||
target datalayout = "e-m:e-p:32:32-i64:64-n32:64-S128-ni:1:10:20"
|
|
||||||
target triple = "wasm32-unknown-wasi"
|
|
||||||
|
|
||||||
%runtime.funcValueWithSignature = type { i32, i8* }
|
|
||||||
%runtime.channel = type { i32, i32, i8, %runtime.channelBlockedList*, i32, i32, i32, i8* }
|
|
||||||
%runtime.channelBlockedList = type { %runtime.channelBlockedList*, %"internal/task.Task"*, %runtime.chanSelectState*, { %runtime.channelBlockedList*, i32, i32 } }
|
|
||||||
%"internal/task.Task" = type { %"internal/task.Task"*, i8*, i64, %"internal/task.gcData", %"internal/task.state" }
|
|
||||||
%"internal/task.gcData" = type {}
|
|
||||||
%"internal/task.state" = type { i8* }
|
|
||||||
%runtime.chanSelectState = type { %runtime.channel*, i8* }
|
|
||||||
|
|
||||||
@"main$pack" = internal unnamed_addr constant { i32, i8* } { i32 5, i8* undef }
|
|
||||||
@"main$pack.1" = internal unnamed_addr constant { i32, i8* } { i32 5, i8* undef }
|
|
||||||
@"reflect/types.funcid:func:{basic:int}{}" = external constant i8
|
|
||||||
@"main.closureFunctionGoroutine$1$withSignature" = linkonce_odr constant %runtime.funcValueWithSignature { i32 ptrtoint (void (i32, i8*, i8*)* @"main.closureFunctionGoroutine$1" to i32), i8* @"reflect/types.funcid:func:{basic:int}{}" }
|
|
||||||
@"main$string" = internal unnamed_addr constant [4 x i8] c"test", align 1
|
|
||||||
|
|
||||||
declare noalias nonnull i8* @runtime.alloc(i32, i8*, i8*, i8*)
|
|
||||||
|
|
||||||
declare void @runtime.trackPointer(i8* nocapture readonly, i8*, i8*)
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.init(i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.regularFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
call void @"internal/task.start"(i32 ptrtoint (void (i32, i8*, i8*)* @main.regularFunction to i32), i8* bitcast ({ i32, i8* }* @"main$pack" to i8*), i32 undef, i8* undef, i8* null) #0
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare void @main.regularFunction(i32, i8*, i8*)
|
|
||||||
|
|
||||||
declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*)
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.inlineFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
call void @"internal/task.start"(i32 ptrtoint (void (i32, i8*, i8*)* @"main.inlineFunctionGoroutine$1" to i32), i8* bitcast ({ i32, i8* }* @"main$pack.1" to i8*), i32 undef, i8* undef, i8* null) #0
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @"main.inlineFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.closureFunctionGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
%n = call i8* @runtime.alloc(i32 4, i8* nonnull inttoptr (i32 3 to i8*), i8* undef, i8* null) #0
|
|
||||||
%0 = bitcast i8* %n to i32*
|
|
||||||
call void @runtime.trackPointer(i8* nonnull %n, i8* undef, i8* null) #0
|
|
||||||
store i32 3, i32* %0, align 4
|
|
||||||
call void @runtime.trackPointer(i8* nonnull %n, i8* undef, i8* null) #0
|
|
||||||
%1 = call i8* @runtime.alloc(i32 8, i8* null, i8* undef, i8* null) #0
|
|
||||||
call void @runtime.trackPointer(i8* nonnull %1, i8* undef, i8* null) #0
|
|
||||||
%2 = bitcast i8* %1 to i32*
|
|
||||||
store i32 5, i32* %2, align 4
|
|
||||||
%3 = getelementptr inbounds i8, i8* %1, i32 4
|
|
||||||
%4 = bitcast i8* %3 to i8**
|
|
||||||
store i8* %n, i8** %4, align 4
|
|
||||||
call void @"internal/task.start"(i32 ptrtoint (void (i32, i8*, i8*)* @"main.closureFunctionGoroutine$1" to i32), i8* nonnull %1, i32 undef, i8* undef, i8* null) #0
|
|
||||||
%5 = load i32, i32* %0, align 4
|
|
||||||
call void @runtime.printint32(i32 %5, i8* undef, i8* null) #0
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @"main.closureFunctionGoroutine$1"(i32 %x, i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
%unpack.ptr = bitcast i8* %context to i32*
|
|
||||||
store i32 7, i32* %unpack.ptr, align 4
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare void @runtime.printint32(i32, i8*, i8*)
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.funcGoroutine(i8* %fn.context, i32 %fn.funcptr, i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
%0 = call i32 @runtime.getFuncPtr(i8* %fn.context, i32 %fn.funcptr, i8* nonnull @"reflect/types.funcid:func:{basic:int}{}", i8* undef, i8* null) #0
|
|
||||||
%1 = call i8* @runtime.alloc(i32 8, i8* null, i8* undef, i8* null) #0
|
|
||||||
call void @runtime.trackPointer(i8* nonnull %1, i8* undef, i8* null) #0
|
|
||||||
%2 = bitcast i8* %1 to i32*
|
|
||||||
store i32 5, i32* %2, align 4
|
|
||||||
%3 = getelementptr inbounds i8, i8* %1, i32 4
|
|
||||||
%4 = bitcast i8* %3 to i8**
|
|
||||||
store i8* %fn.context, i8** %4, align 4
|
|
||||||
call void @"internal/task.start"(i32 %0, i8* nonnull %1, i32 undef, i8* undef, i8* null) #0
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare i32 @runtime.getFuncPtr(i8*, i32, i8* dereferenceable_or_null(1), i8*, i8*)
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.recoverBuiltinGoroutine(i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.copyBuiltinGoroutine(i8* %dst.data, i32 %dst.len, i32 %dst.cap, i8* %src.data, i32 %src.len, i32 %src.cap, i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
%copy.n = call i32 @runtime.sliceCopy(i8* %dst.data, i8* %src.data, i32 %dst.len, i32 %src.len, i32 1, i8* undef, i8* null) #0
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare i32 @runtime.sliceCopy(i8* nocapture writeonly, i8* nocapture readonly, i32, i32, i32, i8*, i8*)
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.closeBuiltinGoroutine(%runtime.channel* dereferenceable_or_null(32) %ch, i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
call void @runtime.chanClose(%runtime.channel* %ch, i8* undef, i8* null) #0
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare void @runtime.chanClose(%runtime.channel* dereferenceable_or_null(32), i8*, i8*)
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
define hidden void @main.startInterfaceMethod(i32 %itf.typecode, i8* %itf.value, i8* %context, i8* %parentHandle) unnamed_addr #0 {
|
|
||||||
entry:
|
|
||||||
%0 = call i8* @runtime.alloc(i32 16, i8* null, i8* undef, i8* null) #0
|
|
||||||
call void @runtime.trackPointer(i8* nonnull %0, i8* undef, i8* null) #0
|
|
||||||
%1 = bitcast i8* %0 to i8**
|
|
||||||
store i8* %itf.value, i8** %1, align 4
|
|
||||||
%2 = getelementptr inbounds i8, i8* %0, i32 4
|
|
||||||
%.repack = bitcast i8* %2 to i8**
|
|
||||||
store i8* getelementptr inbounds ([4 x i8], [4 x i8]* @"main$string", i32 0, i32 0), i8** %.repack, align 4
|
|
||||||
%.repack1 = getelementptr inbounds i8, i8* %0, i32 8
|
|
||||||
%3 = bitcast i8* %.repack1 to i32*
|
|
||||||
store i32 4, i32* %3, align 4
|
|
||||||
%4 = getelementptr inbounds i8, i8* %0, i32 12
|
|
||||||
%5 = bitcast i8* %4 to i32*
|
|
||||||
store i32 %itf.typecode, i32* %5, align 4
|
|
||||||
call void @"internal/task.start"(i32 ptrtoint (void (i8*, i8*, i32, i32, i8*, i8*)* @"interface:{Print:func:{basic:string}{}}.Print$invoke" to i32), i8* nonnull %0, i32 undef, i8* undef, i8* null) #0
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
declare void @"interface:{Print:func:{basic:string}{}}.Print$invoke"(i8*, i8*, i32, i32, i8*, i8*) #1
|
|
||||||
|
|
||||||
attributes #0 = { nounwind }
|
|
||||||
attributes #1 = { "tinygo-invoke"="reflect/methods.Print(string)" "tinygo-methods"="reflect/methods.Print(string)" }
|
|
12
main.go
12
main.go
|
@ -30,7 +30,6 @@ import (
|
||||||
"github.com/tinygo-org/tinygo/goenv"
|
"github.com/tinygo-org/tinygo/goenv"
|
||||||
"github.com/tinygo-org/tinygo/interp"
|
"github.com/tinygo-org/tinygo/interp"
|
||||||
"github.com/tinygo-org/tinygo/loader"
|
"github.com/tinygo-org/tinygo/loader"
|
||||||
"github.com/tinygo-org/tinygo/transform"
|
|
||||||
"tinygo.org/x/go-llvm"
|
"tinygo.org/x/go-llvm"
|
||||||
|
|
||||||
"go.bug.st/serial"
|
"go.bug.st/serial"
|
||||||
|
@ -1100,15 +1099,6 @@ func printCompilerError(logln func(...interface{}), err error) {
|
||||||
logln()
|
logln()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case transform.CoroutinesError:
|
|
||||||
logln(err.Pos.String() + ": " + err.Msg)
|
|
||||||
logln("\ntraceback:")
|
|
||||||
for _, line := range err.Traceback {
|
|
||||||
logln(line.Name)
|
|
||||||
if line.Position.IsValid() {
|
|
||||||
logln("\t" + line.Position.String())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case loader.Errors:
|
case loader.Errors:
|
||||||
logln("#", err.Pkg.ImportPath)
|
logln("#", err.Pkg.ImportPath)
|
||||||
for _, err := range err.Errs {
|
for _, err := range err.Errs {
|
||||||
|
@ -1195,7 +1185,7 @@ func main() {
|
||||||
opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z")
|
opt := flag.String("opt", "z", "optimization level: 0, 1, 2, s, z")
|
||||||
gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative)")
|
gc := flag.String("gc", "", "garbage collector to use (none, leaking, conservative)")
|
||||||
panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)")
|
panicStrategy := flag.String("panic", "print", "panic strategy (print, trap)")
|
||||||
scheduler := flag.String("scheduler", "", "which scheduler to use (none, coroutines, tasks, asyncify)")
|
scheduler := flag.String("scheduler", "", "which scheduler to use (none, tasks, asyncify)")
|
||||||
serial := flag.String("serial", "", "which serial output to use (none, uart, usb)")
|
serial := flag.String("serial", "", "which serial output to use (none, uart, usb)")
|
||||||
printIR := flag.Bool("printir", false, "print LLVM IR")
|
printIR := flag.Bool("printir", false, "print LLVM IR")
|
||||||
dumpSSA := flag.Bool("dumpssa", false, "dump internal Go SSA")
|
dumpSSA := flag.Bool("dumpssa", false, "dump internal Go SSA")
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//go:build gc.conservative && tinygo.wasm && !scheduler.coroutines
|
//go:build gc.conservative && tinygo.wasm
|
||||||
// +build gc.conservative,tinygo.wasm,!scheduler.coroutines
|
// +build gc.conservative,tinygo.wasm
|
||||||
|
|
||||||
package task
|
package task
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
//go:build !gc.conservative || !tinygo.wasm || scheduler.coroutines
|
//go:build !gc.conservative || !tinygo.wasm
|
||||||
// +build !gc.conservative !tinygo.wasm scheduler.coroutines
|
// +build !gc.conservative !tinygo.wasm
|
||||||
|
|
||||||
package task
|
package task
|
||||||
|
|
||||||
|
|
|
@ -1,90 +0,0 @@
|
||||||
// +build scheduler.coroutines
|
|
||||||
|
|
||||||
package task
|
|
||||||
|
|
||||||
import (
|
|
||||||
"unsafe"
|
|
||||||
)
|
|
||||||
|
|
||||||
// rawState is an underlying coroutine state exposed by llvm.coro.
|
|
||||||
// This matches *i8 in LLVM.
|
|
||||||
type rawState uint8
|
|
||||||
|
|
||||||
//export llvm.coro.resume
|
|
||||||
func coroResume(*rawState)
|
|
||||||
|
|
||||||
type state struct{ *rawState }
|
|
||||||
|
|
||||||
//export llvm.coro.noop
|
|
||||||
func noopState() *rawState
|
|
||||||
|
|
||||||
// Resume the task until it pauses or completes.
|
|
||||||
func (t *Task) Resume() {
|
|
||||||
coroResume(t.state.rawState)
|
|
||||||
}
|
|
||||||
|
|
||||||
// setState is used by the compiler to set the state of the function at the beginning of a function call.
|
|
||||||
// Returns the state of the caller.
|
|
||||||
func (t *Task) setState(s *rawState) *rawState {
|
|
||||||
caller := t.state
|
|
||||||
t.state = state{s}
|
|
||||||
return caller.rawState
|
|
||||||
}
|
|
||||||
|
|
||||||
// returnTo is used by the compiler to return to the state of the caller.
|
|
||||||
func (t *Task) returnTo(parent *rawState) {
|
|
||||||
t.state = state{parent}
|
|
||||||
t.returnCurrent()
|
|
||||||
}
|
|
||||||
|
|
||||||
// returnCurrent is used by the compiler to return to the state of the caller in a case where the state is not replaced.
|
|
||||||
func (t *Task) returnCurrent() {
|
|
||||||
scheduleTask(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
//go:linkname scheduleTask runtime.runqueuePushBack
|
|
||||||
func scheduleTask(*Task)
|
|
||||||
|
|
||||||
// setReturnPtr is used by the compiler to store the return buffer into the task.
|
|
||||||
// This buffer is where the return value of a function that is about to be called will be stored.
|
|
||||||
func (t *Task) setReturnPtr(buf unsafe.Pointer) {
|
|
||||||
t.Ptr = buf
|
|
||||||
}
|
|
||||||
|
|
||||||
// getReturnPtr is used by the compiler to get the return buffer stored into the task.
|
|
||||||
// This is called at the beginning of an async function, and the return is stored into this buffer immediately before resuming the caller.
|
|
||||||
func (t *Task) getReturnPtr() unsafe.Pointer {
|
|
||||||
return t.Ptr
|
|
||||||
}
|
|
||||||
|
|
||||||
// createTask returns a new task struct initialized with a no-op state.
|
|
||||||
func createTask() *Task {
|
|
||||||
return &Task{
|
|
||||||
state: state{noopState()},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// start invokes a function in a new goroutine. Calls to this are inserted by the compiler.
|
|
||||||
// The created goroutine starts running immediately.
|
|
||||||
// This is implemented inside the compiler.
|
|
||||||
func start(fn uintptr, args unsafe.Pointer, stackSize uintptr)
|
|
||||||
|
|
||||||
// Current returns the current active task.
|
|
||||||
// This is implemented inside the compiler.
|
|
||||||
func Current() *Task
|
|
||||||
|
|
||||||
// Pause suspends the current running task.
|
|
||||||
// This is implemented inside the compiler.
|
|
||||||
func Pause()
|
|
||||||
|
|
||||||
func fake() {
|
|
||||||
// Hack to ensure intrinsics are discovered.
|
|
||||||
Current()
|
|
||||||
Pause()
|
|
||||||
}
|
|
||||||
|
|
||||||
// OnSystemStack returns whether the caller is running on the system stack.
|
|
||||||
func OnSystemStack() bool {
|
|
||||||
// This scheduler does not do any stack switching.
|
|
||||||
return true
|
|
||||||
}
|
|
|
@ -15,13 +15,13 @@ package runtime
|
||||||
// closed:
|
// closed:
|
||||||
// The channel is closed. Sends will panic, receives will get a zero value
|
// The channel is closed. Sends will panic, receives will get a zero value
|
||||||
// plus optionally the indication that the channel is zero (with the
|
// plus optionally the indication that the channel is zero (with the
|
||||||
// commao-ok value in the coroutine).
|
// comma-ok value in the task).
|
||||||
//
|
//
|
||||||
// A send/recv transmission is completed by copying from the data element of the
|
// A send/recv transmission is completed by copying from the data element of the
|
||||||
// sending coroutine to the data element of the receiving coroutine, and setting
|
// sending task to the data element of the receiving task, and setting
|
||||||
// the 'comma-ok' value to true.
|
// the 'comma-ok' value to true.
|
||||||
// A receive operation on a closed channel is completed by zeroing the data
|
// A receive operation on a closed channel is completed by zeroing the data
|
||||||
// element of the receiving coroutine and setting the 'comma-ok' value to false.
|
// element of the receiving task and setting the 'comma-ok' value to false.
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"internal/task"
|
"internal/task"
|
||||||
|
|
|
@ -6,13 +6,9 @@ package runtime
|
||||||
// were added to the queue (first-in, first-out). It also contains a sleep queue
|
// were added to the queue (first-in, first-out). It also contains a sleep queue
|
||||||
// with sleeping goroutines in order of when they should be re-activated.
|
// with sleeping goroutines in order of when they should be re-activated.
|
||||||
//
|
//
|
||||||
// The scheduler is used both for the coroutine based scheduler and for the task
|
// The scheduler is used both for the asyncify based scheduler and for the task
|
||||||
// based scheduler (see compiler/goroutine-lowering.go for a description). In
|
// based scheduler. In both cases, the 'internal/task.Task' type is used to represent one
|
||||||
// both cases, the 'task' type is used to represent one goroutine. In the case
|
// goroutine.
|
||||||
// of the task based scheduler, it literally is the goroutine itself: a pointer
|
|
||||||
// to the bottom of the stack where some important fields are kept. In the case
|
|
||||||
// of the coroutine-based scheduler, it is the coroutine pointer (a *i8 in
|
|
||||||
// LLVM).
|
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"internal/task"
|
"internal/task"
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
// +build scheduler.coroutines
|
|
||||||
|
|
||||||
package runtime
|
|
||||||
|
|
||||||
// getSystemStackPointer returns the current stack pointer of the system stack.
|
|
||||||
// This is always the current stack pointer.
|
|
||||||
func getSystemStackPointer() uintptr {
|
|
||||||
return getCurrentStackPointer()
|
|
||||||
}
|
|
2
testdata/goroutines.go
предоставленный
2
testdata/goroutines.go
предоставленный
|
@ -20,7 +20,7 @@ func main() {
|
||||||
time.Sleep(2 * time.Millisecond)
|
time.Sleep(2 * time.Millisecond)
|
||||||
println("main 3")
|
println("main 3")
|
||||||
|
|
||||||
// Await a blocking call. This must create a new coroutine.
|
// Await a blocking call.
|
||||||
println("wait:")
|
println("wait:")
|
||||||
wait()
|
wait()
|
||||||
println("end waiting")
|
println("end waiting")
|
||||||
|
|
Различия файлов не показаны, т.к. их слишком много
Показать различия
|
@ -1,18 +0,0 @@
|
||||||
package transform_test
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/tinygo-org/tinygo/transform"
|
|
||||||
"tinygo.org/x/go-llvm"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestGoroutineLowering(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
testTransform(t, "testdata/coroutines", func(mod llvm.Module) {
|
|
||||||
err := transform.LowerCoroutines(mod, false)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -29,10 +29,9 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i
|
||||||
if inlinerThreshold != 0 {
|
if inlinerThreshold != 0 {
|
||||||
builder.UseInlinerWithThreshold(inlinerThreshold)
|
builder.UseInlinerWithThreshold(inlinerThreshold)
|
||||||
}
|
}
|
||||||
builder.AddCoroutinePassesToExtensionPoints()
|
|
||||||
|
|
||||||
// Make sure these functions are kept in tact during TinyGo transformation passes.
|
// Make sure these functions are kept in tact during TinyGo transformation passes.
|
||||||
for _, name := range getFunctionsUsedInTransforms(config) {
|
for _, name := range functionsUsedInTransforms {
|
||||||
fn := mod.NamedFunction(name)
|
fn := mod.NamedFunction(name)
|
||||||
if fn.IsNil() {
|
if fn.IsNil() {
|
||||||
panic(fmt.Errorf("missing core function %q", name))
|
panic(fmt.Errorf("missing core function %q", name))
|
||||||
|
@ -118,17 +117,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i
|
||||||
goPasses.Run(mod)
|
goPasses.Run(mod)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lower async implementations.
|
if config.Scheduler() == "none" {
|
||||||
switch config.Scheduler() {
|
|
||||||
case "coroutines":
|
|
||||||
// Lower async as coroutines.
|
|
||||||
err := LowerCoroutines(mod, config.NeedsStackObjects())
|
|
||||||
if err != nil {
|
|
||||||
return []error{err}
|
|
||||||
}
|
|
||||||
case "tasks", "asyncify":
|
|
||||||
// No transformations necessary.
|
|
||||||
case "none":
|
|
||||||
// Check for any goroutine starts.
|
// Check for any goroutine starts.
|
||||||
if start := mod.NamedFunction("internal/task.start"); !start.IsNil() && len(getUses(start)) > 0 {
|
if start := mod.NamedFunction("internal/task.start"); !start.IsNil() && len(getUses(start)) > 0 {
|
||||||
errs := []error{}
|
errs := []error{}
|
||||||
|
@ -137,8 +126,6 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i
|
||||||
}
|
}
|
||||||
return errs
|
return errs
|
||||||
}
|
}
|
||||||
default:
|
|
||||||
return []error{errors.New("invalid scheduler")}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.VerifyIR() {
|
if config.VerifyIR() {
|
||||||
|
@ -151,7 +138,7 @@ func Optimize(mod llvm.Module, config *compileopts.Config, optLevel, sizeLevel i
|
||||||
}
|
}
|
||||||
|
|
||||||
// After TinyGo-specific transforms have finished, undo exporting these functions.
|
// After TinyGo-specific transforms have finished, undo exporting these functions.
|
||||||
for _, name := range getFunctionsUsedInTransforms(config) {
|
for _, name := range functionsUsedInTransforms {
|
||||||
fn := mod.NamedFunction(name)
|
fn := mod.NamedFunction(name)
|
||||||
if fn.IsNil() || fn.IsDeclaration() {
|
if fn.IsNil() || fn.IsDeclaration() {
|
||||||
continue
|
continue
|
||||||
|
@ -195,35 +182,3 @@ var functionsUsedInTransforms = []string{
|
||||||
"runtime.free",
|
"runtime.free",
|
||||||
"runtime.nilPanic",
|
"runtime.nilPanic",
|
||||||
}
|
}
|
||||||
|
|
||||||
var taskFunctionsUsedInTransforms = []string{}
|
|
||||||
|
|
||||||
// These functions need to be preserved in the IR until after the coroutines
|
|
||||||
// pass has run.
|
|
||||||
var coroFunctionsUsedInTransforms = []string{
|
|
||||||
"internal/task.start",
|
|
||||||
"internal/task.Pause",
|
|
||||||
"internal/task.fake",
|
|
||||||
"internal/task.Current",
|
|
||||||
"internal/task.createTask",
|
|
||||||
"(*internal/task.Task).setState",
|
|
||||||
"(*internal/task.Task).returnTo",
|
|
||||||
"(*internal/task.Task).returnCurrent",
|
|
||||||
"(*internal/task.Task).setReturnPtr",
|
|
||||||
"(*internal/task.Task).getReturnPtr",
|
|
||||||
}
|
|
||||||
|
|
||||||
// getFunctionsUsedInTransforms gets a list of all special functions that should be preserved during transforms and optimization.
|
|
||||||
func getFunctionsUsedInTransforms(config *compileopts.Config) []string {
|
|
||||||
fnused := functionsUsedInTransforms
|
|
||||||
switch config.Scheduler() {
|
|
||||||
case "none":
|
|
||||||
case "coroutines":
|
|
||||||
fnused = append(append([]string{}, fnused...), coroFunctionsUsedInTransforms...)
|
|
||||||
case "tasks", "asyncify":
|
|
||||||
fnused = append(append([]string{}, fnused...), taskFunctionsUsedInTransforms...)
|
|
||||||
default:
|
|
||||||
panic(fmt.Errorf("invalid scheduler %q", config.Scheduler()))
|
|
||||||
}
|
|
||||||
return fnused
|
|
||||||
}
|
|
||||||
|
|
152
transform/testdata/coroutines.ll
предоставленный
152
transform/testdata/coroutines.ll
предоставленный
|
@ -1,152 +0,0 @@
|
||||||
target datalayout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64"
|
|
||||||
target triple = "armv7m-none-eabi"
|
|
||||||
|
|
||||||
%"internal/task.state" = type { i8* }
|
|
||||||
%"internal/task.Task" = type { %"internal/task.Task", i8*, i32, %"internal/task.state" }
|
|
||||||
|
|
||||||
declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*)
|
|
||||||
declare void @"internal/task.Pause"(i8*, i8*)
|
|
||||||
|
|
||||||
declare void @runtime.scheduler(i8*, i8*)
|
|
||||||
|
|
||||||
declare i8* @runtime.alloc(i32, i8*, i8*, i8*)
|
|
||||||
declare void @runtime.free(i8*, i8*, i8*)
|
|
||||||
|
|
||||||
declare %"internal/task.Task"* @"internal/task.Current"(i8*, i8*)
|
|
||||||
|
|
||||||
declare i8* @"(*internal/task.Task).setState"(%"internal/task.Task"*, i8*, i8*, i8*)
|
|
||||||
declare void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"*, i8*, i8*, i8*)
|
|
||||||
declare i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"*, i8*, i8*)
|
|
||||||
declare void @"(*internal/task.Task).returnTo"(%"internal/task.Task"*, i8*, i8*, i8*)
|
|
||||||
declare void @"(*internal/task.Task).returnCurrent"(%"internal/task.Task"*, i8*, i8*)
|
|
||||||
declare %"internal/task.Task"* @"internal/task.createTask"(i8*, i8*)
|
|
||||||
|
|
||||||
declare void @callMain(i8*, i8*)
|
|
||||||
|
|
||||||
; Test a simple sleep-like scenario.
|
|
||||||
declare void @enqueueTimer(%"internal/task.Task"*, i64, i8*, i8*)
|
|
||||||
|
|
||||||
define void @sleep(i64, i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%2 = call %"internal/task.Task"* @"internal/task.Current"(i8* undef, i8* null)
|
|
||||||
call void @enqueueTimer(%"internal/task.Task"* %2, i64 %0, i8* undef, i8* null)
|
|
||||||
call void @"internal/task.Pause"(i8* undef, i8* null)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Test a delayed value return.
|
|
||||||
define i32 @delayedValue(i32, i64, i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
call void @sleep(i64 %1, i8* undef, i8* null)
|
|
||||||
ret i32 %0
|
|
||||||
}
|
|
||||||
|
|
||||||
; Test a deadlocking async func.
|
|
||||||
define void @deadlock(i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
call void @"internal/task.Pause"(i8* undef, i8* null)
|
|
||||||
unreachable
|
|
||||||
}
|
|
||||||
|
|
||||||
; Test a regular tail call.
|
|
||||||
define i32 @tail(i32, i64, i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* null)
|
|
||||||
ret i32 %3
|
|
||||||
}
|
|
||||||
|
|
||||||
; Test a ditching tail call.
|
|
||||||
define void @ditchTail(i32, i64, i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* null)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Test a void tail call.
|
|
||||||
define void @voidTail(i32, i64, i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
call void @ditchTail(i32 %0, i64 %1, i8* undef, i8* null)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Test a tail call returning an alternate value.
|
|
||||||
define i32 @alternateTail(i32, i32, i64, i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%4 = call i32 @delayedValue(i32 %1, i64 %2, i8* undef, i8* null)
|
|
||||||
ret i32 %0
|
|
||||||
}
|
|
||||||
|
|
||||||
; Test a normal return from a coroutine.
|
|
||||||
; This must be turned into a coroutine.
|
|
||||||
define i1 @coroutine(i32, i64, i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* null)
|
|
||||||
%4 = icmp eq i32 %3, 0
|
|
||||||
ret i1 %4
|
|
||||||
}
|
|
||||||
|
|
||||||
; Normal function which should not be transformed.
|
|
||||||
define void @doNothing(i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Regression test: ensure that a tail call does not destroy the frame while it is still in use.
|
|
||||||
; Previously, the tail-call lowering transform would branch to the cleanup block after usePtr.
|
|
||||||
; This caused the lifetime of %a to be incorrectly reduced, and allowed the coroutine lowering transform to keep %a on the stack.
|
|
||||||
; After a suspend %a would be used, resulting in memory corruption.
|
|
||||||
define i8 @coroutineTailRegression(i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%a = alloca i8
|
|
||||||
store i8 5, i8* %a
|
|
||||||
%val = call i8 @usePtr(i8* %a, i8* undef, i8* null)
|
|
||||||
ret i8 %val
|
|
||||||
}
|
|
||||||
|
|
||||||
; Regression test: ensure that stack allocations alive during a suspend end up on the heap.
|
|
||||||
; This used to not be transformed to a coroutine, keeping %a on the stack.
|
|
||||||
; After a suspend %a would be used, resulting in memory corruption.
|
|
||||||
define i8 @allocaTailRegression(i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%a = alloca i8
|
|
||||||
call void @sleep(i64 1000000, i8* undef, i8* null)
|
|
||||||
store i8 5, i8* %a
|
|
||||||
%val = call i8 @usePtr(i8* %a, i8* undef, i8* null)
|
|
||||||
ret i8 %val
|
|
||||||
}
|
|
||||||
|
|
||||||
; usePtr uses a pointer after a suspend.
|
|
||||||
define i8 @usePtr(i8*, i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
call void @sleep(i64 1000000, i8* undef, i8* null)
|
|
||||||
%val = load i8, i8* %0
|
|
||||||
ret i8 %val
|
|
||||||
}
|
|
||||||
|
|
||||||
; Goroutine that sleeps and does nothing.
|
|
||||||
; Should be a void tail call.
|
|
||||||
define void @sleepGoroutine(i8*, i8* %parentHandle) {
|
|
||||||
call void @sleep(i64 1000000, i8* undef, i8* null)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Program main function.
|
|
||||||
define void @progMain(i8*, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
; Call a sync func in a goroutine.
|
|
||||||
call void @"internal/task.start"(i32 ptrtoint (void (i8*, i8*)* @doNothing to i32), i8* undef, i32 undef, i8* undef, i8* null)
|
|
||||||
; Call an async func in a goroutine.
|
|
||||||
call void @"internal/task.start"(i32 ptrtoint (void (i8*, i8*)* @sleepGoroutine to i32), i8* undef, i32 undef, i8* undef, i8* null)
|
|
||||||
; Sleep a bit.
|
|
||||||
call void @sleep(i64 2000000, i8* undef, i8* null)
|
|
||||||
; Done.
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Entrypoint of runtime.
|
|
||||||
define void @main() {
|
|
||||||
entry:
|
|
||||||
call void @"internal/task.start"(i32 ptrtoint (void (i8*, i8*)* @progMain to i32), i8* undef, i32 undef, i8* undef, i8* null)
|
|
||||||
call void @runtime.scheduler(i8* undef, i8* null)
|
|
||||||
ret void
|
|
||||||
}
|
|
313
transform/testdata/coroutines.out.ll
предоставленный
313
transform/testdata/coroutines.out.ll
предоставленный
|
@ -1,313 +0,0 @@
|
||||||
target datalayout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64"
|
|
||||||
target triple = "armv7m-none-eabi"
|
|
||||||
|
|
||||||
%"internal/task.Task" = type { %"internal/task.Task", i8*, i32, %"internal/task.state" }
|
|
||||||
%"internal/task.state" = type { i8* }
|
|
||||||
|
|
||||||
declare void @"internal/task.start"(i32, i8*, i32, i8*, i8*)
|
|
||||||
|
|
||||||
declare void @"internal/task.Pause"(i8*, i8*)
|
|
||||||
|
|
||||||
declare void @runtime.scheduler(i8*, i8*)
|
|
||||||
|
|
||||||
declare i8* @runtime.alloc(i32, i8*, i8*, i8*)
|
|
||||||
|
|
||||||
declare void @runtime.free(i8*, i8*, i8*)
|
|
||||||
|
|
||||||
declare %"internal/task.Task"* @"internal/task.Current"(i8*, i8*)
|
|
||||||
|
|
||||||
declare i8* @"(*internal/task.Task).setState"(%"internal/task.Task"*, i8*, i8*, i8*)
|
|
||||||
|
|
||||||
declare void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"*, i8*, i8*, i8*)
|
|
||||||
|
|
||||||
declare i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"*, i8*, i8*)
|
|
||||||
|
|
||||||
declare void @"(*internal/task.Task).returnTo"(%"internal/task.Task"*, i8*, i8*, i8*)
|
|
||||||
|
|
||||||
declare void @"(*internal/task.Task).returnCurrent"(%"internal/task.Task"*, i8*, i8*)
|
|
||||||
|
|
||||||
declare %"internal/task.Task"* @"internal/task.createTask"(i8*, i8*)
|
|
||||||
|
|
||||||
declare void @callMain(i8*, i8*)
|
|
||||||
|
|
||||||
declare void @enqueueTimer(%"internal/task.Task"*, i64, i8*, i8*)
|
|
||||||
|
|
||||||
define void @sleep(i64 %0, i8* %1, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%task.current1 = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
call void @enqueueTimer(%"internal/task.Task"* %task.current1, i64 %0, i8* undef, i8* null)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
define i32 @delayedValue(i32 %0, i64 %1, i8* %2, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%ret.ptr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef)
|
|
||||||
%ret.ptr.bitcast = bitcast i8* %ret.ptr to i32*
|
|
||||||
store i32 %0, i32* %ret.ptr.bitcast, align 4
|
|
||||||
call void @sleep(i64 %1, i8* undef, i8* %parentHandle)
|
|
||||||
ret i32 undef
|
|
||||||
}
|
|
||||||
|
|
||||||
define void @deadlock(i8* %0, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
define i32 @tail(i32 %0, i64 %1, i8* %2, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* %parentHandle)
|
|
||||||
ret i32 undef
|
|
||||||
}
|
|
||||||
|
|
||||||
define void @ditchTail(i32 %0, i64 %1, i8* %2, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%ret.ditch = call i8* @runtime.alloc(i32 4, i8* null, i8* undef, i8* undef)
|
|
||||||
call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %ret.ditch, i8* undef, i8* undef)
|
|
||||||
%3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* %parentHandle)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
define void @voidTail(i32 %0, i64 %1, i8* %2, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
call void @ditchTail(i32 %0, i64 %1, i8* undef, i8* %parentHandle)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
define i32 @alternateTail(i32 %0, i32 %1, i64 %2, i8* %3, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%ret.ptr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef)
|
|
||||||
%ret.ptr.bitcast = bitcast i8* %ret.ptr to i32*
|
|
||||||
store i32 %0, i32* %ret.ptr.bitcast, align 4
|
|
||||||
%ret.alternate = call i8* @runtime.alloc(i32 4, i8* null, i8* undef, i8* undef)
|
|
||||||
call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %ret.alternate, i8* undef, i8* undef)
|
|
||||||
%4 = call i32 @delayedValue(i32 %1, i64 %2, i8* undef, i8* %parentHandle)
|
|
||||||
ret i32 undef
|
|
||||||
}
|
|
||||||
|
|
||||||
define i1 @coroutine(i32 %0, i64 %1, i8* %2, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%call.return = alloca i32, align 4
|
|
||||||
%coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null)
|
|
||||||
%coro.size = call i32 @llvm.coro.size.i32()
|
|
||||||
%coro.alloc = call i8* @runtime.alloc(i32 %coro.size, i8* null, i8* undef, i8* undef)
|
|
||||||
%coro.state = call i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc)
|
|
||||||
%task.current2 = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%task.state.parent = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current2, i8* %coro.state, i8* undef, i8* undef)
|
|
||||||
%task.retPtr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current2, i8* undef, i8* undef)
|
|
||||||
%task.retPtr.bitcast = bitcast i8* %task.retPtr to i1*
|
|
||||||
%call.return.bitcast = bitcast i32* %call.return to i8*
|
|
||||||
call void @llvm.lifetime.start.p0i8(i64 4, i8* %call.return.bitcast)
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%call.return.bitcast1 = bitcast i32* %call.return to i8*
|
|
||||||
call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %call.return.bitcast1, i8* undef, i8* undef)
|
|
||||||
%3 = call i32 @delayedValue(i32 %0, i64 %1, i8* undef, i8* %parentHandle)
|
|
||||||
%coro.save = call token @llvm.coro.save(i8* %coro.state)
|
|
||||||
%call.suspend = call i8 @llvm.coro.suspend(token %coro.save, i1 false)
|
|
||||||
switch i8 %call.suspend, label %suspend [
|
|
||||||
i8 0, label %wakeup
|
|
||||||
i8 1, label %cleanup
|
|
||||||
]
|
|
||||||
|
|
||||||
wakeup: ; preds = %entry
|
|
||||||
%4 = load i32, i32* %call.return, align 4
|
|
||||||
call void @llvm.lifetime.end.p0i8(i64 4, i8* %call.return.bitcast)
|
|
||||||
%5 = icmp eq i32 %4, 0
|
|
||||||
store i1 %5, i1* %task.retPtr.bitcast, align 1
|
|
||||||
call void @"(*internal/task.Task).returnTo"(%"internal/task.Task"* %task.current2, i8* %task.state.parent, i8* undef, i8* undef)
|
|
||||||
br label %cleanup
|
|
||||||
|
|
||||||
suspend: ; preds = %entry, %cleanup
|
|
||||||
%unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false)
|
|
||||||
ret i1 undef
|
|
||||||
|
|
||||||
cleanup: ; preds = %entry, %wakeup
|
|
||||||
%coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state)
|
|
||||||
call void @runtime.free(i8* %coro.memFree, i8* undef, i8* undef)
|
|
||||||
br label %suspend
|
|
||||||
}
|
|
||||||
|
|
||||||
define void @doNothing(i8* %0, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
define i8 @coroutineTailRegression(i8* %0, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%a = alloca i8, align 1
|
|
||||||
%coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null)
|
|
||||||
%coro.size = call i32 @llvm.coro.size.i32()
|
|
||||||
%coro.alloc = call i8* @runtime.alloc(i32 %coro.size, i8* null, i8* undef, i8* undef)
|
|
||||||
%coro.state = call i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc)
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%task.state.parent = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %coro.state, i8* undef, i8* undef)
|
|
||||||
%task.retPtr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef)
|
|
||||||
store i8 5, i8* %a, align 1
|
|
||||||
%coro.state.restore = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %task.state.parent, i8* undef, i8* undef)
|
|
||||||
call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %task.retPtr, i8* undef, i8* undef)
|
|
||||||
%val = call i8 @usePtr(i8* %a, i8* undef, i8* %parentHandle)
|
|
||||||
br label %post.tail
|
|
||||||
|
|
||||||
suspend: ; preds = %post.tail, %cleanup
|
|
||||||
%unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false)
|
|
||||||
ret i8 undef
|
|
||||||
|
|
||||||
cleanup: ; preds = %post.tail
|
|
||||||
%coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state)
|
|
||||||
call void @runtime.free(i8* %coro.memFree, i8* undef, i8* undef)
|
|
||||||
br label %suspend
|
|
||||||
|
|
||||||
post.tail: ; preds = %entry
|
|
||||||
%coro.save = call token @llvm.coro.save(i8* %coro.state)
|
|
||||||
%call.suspend = call i8 @llvm.coro.suspend(token %coro.save, i1 false)
|
|
||||||
switch i8 %call.suspend, label %suspend [
|
|
||||||
i8 0, label %unreachable
|
|
||||||
i8 1, label %cleanup
|
|
||||||
]
|
|
||||||
|
|
||||||
unreachable: ; preds = %post.tail
|
|
||||||
unreachable
|
|
||||||
}
|
|
||||||
|
|
||||||
define i8 @allocaTailRegression(i8* %0, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%a = alloca i8, align 1
|
|
||||||
%coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null)
|
|
||||||
%coro.size = call i32 @llvm.coro.size.i32()
|
|
||||||
%coro.alloc = call i8* @runtime.alloc(i32 %coro.size, i8* null, i8* undef, i8* undef)
|
|
||||||
%coro.state = call i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc)
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%task.state.parent = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %coro.state, i8* undef, i8* undef)
|
|
||||||
%task.retPtr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef)
|
|
||||||
call void @sleep(i64 1000000, i8* undef, i8* %parentHandle)
|
|
||||||
%coro.save1 = call token @llvm.coro.save(i8* %coro.state)
|
|
||||||
%call.suspend2 = call i8 @llvm.coro.suspend(token %coro.save1, i1 false)
|
|
||||||
switch i8 %call.suspend2, label %suspend [
|
|
||||||
i8 0, label %wakeup
|
|
||||||
i8 1, label %cleanup
|
|
||||||
]
|
|
||||||
|
|
||||||
wakeup: ; preds = %entry
|
|
||||||
store i8 5, i8* %a, align 1
|
|
||||||
%1 = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %task.state.parent, i8* undef, i8* undef)
|
|
||||||
call void @"(*internal/task.Task).setReturnPtr"(%"internal/task.Task"* %task.current, i8* %task.retPtr, i8* undef, i8* undef)
|
|
||||||
%2 = call i8 @usePtr(i8* %a, i8* undef, i8* %parentHandle)
|
|
||||||
br label %post.tail
|
|
||||||
|
|
||||||
suspend: ; preds = %entry, %post.tail, %cleanup
|
|
||||||
%unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false)
|
|
||||||
ret i8 undef
|
|
||||||
|
|
||||||
cleanup: ; preds = %entry, %post.tail
|
|
||||||
%coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state)
|
|
||||||
call void @runtime.free(i8* %coro.memFree, i8* undef, i8* undef)
|
|
||||||
br label %suspend
|
|
||||||
|
|
||||||
post.tail: ; preds = %wakeup
|
|
||||||
%coro.save = call token @llvm.coro.save(i8* %coro.state)
|
|
||||||
%call.suspend = call i8 @llvm.coro.suspend(token %coro.save, i1 false)
|
|
||||||
switch i8 %call.suspend, label %suspend [
|
|
||||||
i8 0, label %unreachable
|
|
||||||
i8 1, label %cleanup
|
|
||||||
]
|
|
||||||
|
|
||||||
unreachable: ; preds = %post.tail
|
|
||||||
unreachable
|
|
||||||
}
|
|
||||||
|
|
||||||
define i8 @usePtr(i8* %0, i8* %1, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%coro.id = call token @llvm.coro.id(i32 0, i8* null, i8* null, i8* null)
|
|
||||||
%coro.size = call i32 @llvm.coro.size.i32()
|
|
||||||
%coro.alloc = call i8* @runtime.alloc(i32 %coro.size, i8* null, i8* undef, i8* undef)
|
|
||||||
%coro.state = call i8* @llvm.coro.begin(token %coro.id, i8* %coro.alloc)
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
%task.state.parent = call i8* @"(*internal/task.Task).setState"(%"internal/task.Task"* %task.current, i8* %coro.state, i8* undef, i8* undef)
|
|
||||||
%task.retPtr = call i8* @"(*internal/task.Task).getReturnPtr"(%"internal/task.Task"* %task.current, i8* undef, i8* undef)
|
|
||||||
call void @sleep(i64 1000000, i8* undef, i8* %parentHandle)
|
|
||||||
%coro.save = call token @llvm.coro.save(i8* %coro.state)
|
|
||||||
%call.suspend = call i8 @llvm.coro.suspend(token %coro.save, i1 false)
|
|
||||||
switch i8 %call.suspend, label %suspend [
|
|
||||||
i8 0, label %wakeup
|
|
||||||
i8 1, label %cleanup
|
|
||||||
]
|
|
||||||
|
|
||||||
wakeup: ; preds = %entry
|
|
||||||
%2 = load i8, i8* %0, align 1
|
|
||||||
store i8 %2, i8* %task.retPtr, align 1
|
|
||||||
call void @"(*internal/task.Task).returnTo"(%"internal/task.Task"* %task.current, i8* %task.state.parent, i8* undef, i8* undef)
|
|
||||||
br label %cleanup
|
|
||||||
|
|
||||||
suspend: ; preds = %entry, %cleanup
|
|
||||||
%unused = call i1 @llvm.coro.end(i8* %coro.state, i1 false)
|
|
||||||
ret i8 undef
|
|
||||||
|
|
||||||
cleanup: ; preds = %entry, %wakeup
|
|
||||||
%coro.memFree = call i8* @llvm.coro.free(token %coro.id, i8* %coro.state)
|
|
||||||
call void @runtime.free(i8* %coro.memFree, i8* undef, i8* undef)
|
|
||||||
br label %suspend
|
|
||||||
}
|
|
||||||
|
|
||||||
define void @sleepGoroutine(i8* %0, i8* %parentHandle) {
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
call void @sleep(i64 1000000, i8* undef, i8* %parentHandle)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
define void @progMain(i8* %0, i8* %parentHandle) {
|
|
||||||
entry:
|
|
||||||
%task.current = bitcast i8* %parentHandle to %"internal/task.Task"*
|
|
||||||
call void @doNothing(i8* undef, i8* undef)
|
|
||||||
%start.task = call %"internal/task.Task"* @"internal/task.createTask"(i8* undef, i8* undef)
|
|
||||||
%start.task.bitcast = bitcast %"internal/task.Task"* %start.task to i8*
|
|
||||||
call void @sleepGoroutine(i8* undef, i8* %start.task.bitcast)
|
|
||||||
call void @sleep(i64 2000000, i8* undef, i8* %parentHandle)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
define void @main() {
|
|
||||||
entry:
|
|
||||||
%start.task = call %"internal/task.Task"* @"internal/task.createTask"(i8* undef, i8* undef)
|
|
||||||
%start.task.bitcast = bitcast %"internal/task.Task"* %start.task to i8*
|
|
||||||
call void @progMain(i8* undef, i8* %start.task.bitcast)
|
|
||||||
call void @runtime.scheduler(i8* undef, i8* null)
|
|
||||||
ret void
|
|
||||||
}
|
|
||||||
|
|
||||||
; Function Attrs: argmemonly nounwind readonly
|
|
||||||
declare token @llvm.coro.id(i32, i8* readnone, i8* nocapture readonly, i8*) #0
|
|
||||||
|
|
||||||
; Function Attrs: nounwind readnone
|
|
||||||
declare i32 @llvm.coro.size.i32() #1
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
declare i8* @llvm.coro.begin(token, i8* writeonly) #2
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
declare i8 @llvm.coro.suspend(token, i1) #2
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
declare i1 @llvm.coro.end(i8*, i1) #2
|
|
||||||
|
|
||||||
; Function Attrs: argmemonly nounwind readonly
|
|
||||||
declare i8* @llvm.coro.free(token, i8* nocapture readonly) #0
|
|
||||||
|
|
||||||
; Function Attrs: nounwind
|
|
||||||
declare token @llvm.coro.save(i8*) #2
|
|
||||||
|
|
||||||
; Function Attrs: argmemonly nofree nosync nounwind willreturn
|
|
||||||
declare void @llvm.lifetime.start.p0i8(i64 immarg, i8* nocapture) #3
|
|
||||||
|
|
||||||
; Function Attrs: argmemonly nofree nosync nounwind willreturn
|
|
||||||
declare void @llvm.lifetime.end.p0i8(i64 immarg, i8* nocapture) #3
|
|
||||||
|
|
||||||
attributes #0 = { argmemonly nounwind readonly }
|
|
||||||
attributes #1 = { nounwind readnone }
|
|
||||||
attributes #2 = { nounwind }
|
|
||||||
attributes #3 = { argmemonly nofree nosync nounwind willreturn }
|
|
|
@ -36,7 +36,7 @@ func ExternalInt64AsPtr(mod llvm.Module, config *compileopts.Config) error {
|
||||||
if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") {
|
if strings.HasPrefix(fn.Name(), "llvm.") || strings.HasPrefix(fn.Name(), "runtime.") {
|
||||||
// Do not try to modify the signature of internal LLVM functions and
|
// Do not try to modify the signature of internal LLVM functions and
|
||||||
// assume that runtime functions are only temporarily exported for
|
// assume that runtime functions are only temporarily exported for
|
||||||
// coroutine lowering.
|
// transforms.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if !fn.GetStringAttributeAtIndex(-1, "tinygo-methods").IsNil() {
|
if !fn.GetStringAttributeAtIndex(-1, "tinygo-methods").IsNil() {
|
||||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче