Preliminary implementation of a hashmap, unfinished
Missing features: * keys other than strings * more than 8 values in the hashmap * growing a map when needed * initial size hint * delete(m, key) * iterators (for range) * initializing global maps * ...more?
Этот коммит содержится в:
родитель
8fb9cd4e23
коммит
3a6ef38041
5 изменённых файлов: 290 добавлений и 21 удалений
|
@ -46,11 +46,11 @@ Currently supported features:
|
||||||
* standard library (but most packages won't work due to missing language
|
* standard library (but most packages won't work due to missing language
|
||||||
features)
|
features)
|
||||||
* slices (partially)
|
* slices (partially)
|
||||||
|
* maps (very rough, unfinished)
|
||||||
|
|
||||||
Not yet supported:
|
Not yet supported:
|
||||||
|
|
||||||
* float, complex, etc.
|
* float, complex, etc.
|
||||||
* maps
|
|
||||||
* garbage collection
|
* garbage collection
|
||||||
* defer
|
* defer
|
||||||
* closures
|
* closures
|
||||||
|
|
151
compiler.go
151
compiler.go
|
@ -431,6 +431,8 @@ func (c *Compiler) getLLVMType(goType types.Type) (llvm.Type, error) {
|
||||||
}
|
}
|
||||||
case *types.Interface:
|
case *types.Interface:
|
||||||
return c.mod.GetTypeByName("interface"), nil
|
return c.mod.GetTypeByName("interface"), nil
|
||||||
|
case *types.Map:
|
||||||
|
return llvm.PointerType(c.mod.GetTypeByName("runtime.hashmap"), 0), nil
|
||||||
case *types.Named:
|
case *types.Named:
|
||||||
if _, ok := typ.Underlying().(*types.Struct); ok {
|
if _, ok := typ.Underlying().(*types.Struct); ok {
|
||||||
llvmType := c.mod.GetTypeByName(typ.Obj().Pkg().Path() + "." + typ.Obj().Name())
|
llvmType := c.mod.GetTypeByName(typ.Obj().Pkg().Path() + "." + typ.Obj().Name())
|
||||||
|
@ -878,6 +880,36 @@ func (c *Compiler) parseInstr(frame *Frame, instr ssa.Instruction) error {
|
||||||
blockJump := frame.blocks[instr.Block().Succs[0]]
|
blockJump := frame.blocks[instr.Block().Succs[0]]
|
||||||
c.builder.CreateBr(blockJump)
|
c.builder.CreateBr(blockJump)
|
||||||
return nil
|
return nil
|
||||||
|
case *ssa.MapUpdate:
|
||||||
|
m, err := c.parseExpr(frame, instr.Map)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
key, err := c.parseExpr(frame, instr.Key)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
value, err := c.parseExpr(frame, instr.Value)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
mapType := instr.Map.Type().Underlying().(*types.Map)
|
||||||
|
switch keyType := mapType.Key().Underlying().(type) {
|
||||||
|
case *types.Basic:
|
||||||
|
if keyType.Kind() == types.String {
|
||||||
|
valueAlloca := c.builder.CreateAlloca(value.Type(), "hashmap.value")
|
||||||
|
c.builder.CreateStore(value, valueAlloca)
|
||||||
|
valuePtr := c.builder.CreateBitCast(valueAlloca, c.i8ptrType, "hashmap.valueptr")
|
||||||
|
params := []llvm.Value{m, key, valuePtr}
|
||||||
|
fn := c.mod.NamedFunction("runtime.hashmapSet")
|
||||||
|
c.builder.CreateCall(fn, params, "")
|
||||||
|
return nil
|
||||||
|
} else {
|
||||||
|
return errors.New("todo: map update key type: " + keyType.String())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("todo: map update key type: " + keyType.String())
|
||||||
|
}
|
||||||
case *ssa.Panic:
|
case *ssa.Panic:
|
||||||
value, err := c.parseExpr(frame, instr.X)
|
value, err := c.parseExpr(frame, instr.X)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1292,13 +1324,6 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
|
||||||
if expr.CommaOk {
|
if expr.CommaOk {
|
||||||
return llvm.Value{}, errors.New("todo: lookup with comma-ok")
|
return llvm.Value{}, errors.New("todo: lookup with comma-ok")
|
||||||
}
|
}
|
||||||
if _, ok := expr.X.Type().(*types.Map); ok {
|
|
||||||
return llvm.Value{}, errors.New("todo: lookup in map")
|
|
||||||
}
|
|
||||||
// Value type must be a string, which is a basic type.
|
|
||||||
if expr.X.Type().(*types.Basic).Kind() != types.String {
|
|
||||||
panic("lookup on non-string?")
|
|
||||||
}
|
|
||||||
value, err := c.parseExpr(frame, expr.X)
|
value, err := c.parseExpr(frame, expr.X)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return llvm.Value{}, nil
|
return llvm.Value{}, nil
|
||||||
|
@ -1307,21 +1332,50 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return llvm.Value{}, nil
|
return llvm.Value{}, nil
|
||||||
}
|
}
|
||||||
|
switch xType := expr.X.Type().(type) {
|
||||||
// Bounds check.
|
case *types.Basic:
|
||||||
// LLVM optimizes this away in most cases.
|
// Value type must be a string, which is a basic type.
|
||||||
if frame.fn.llvmFn.Name() != "runtime.lookupBoundsCheck" {
|
if xType.Kind() != types.String {
|
||||||
length, err := c.parseBuiltin(frame, []ssa.Value{expr.X}, "len")
|
panic("lookup on non-string?")
|
||||||
if err != nil {
|
|
||||||
return llvm.Value{}, err // shouldn't happen
|
|
||||||
}
|
}
|
||||||
c.builder.CreateCall(c.mod.NamedFunction("runtime.lookupBoundsCheck"), []llvm.Value{length, index}, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Lookup byte
|
// Bounds check.
|
||||||
buf := c.builder.CreateExtractValue(value, 1, "")
|
// LLVM optimizes this away in most cases.
|
||||||
bufPtr := c.builder.CreateGEP(buf, []llvm.Value{index}, "")
|
if frame.fn.llvmFn.Name() != "runtime.lookupBoundsCheck" {
|
||||||
return c.builder.CreateLoad(bufPtr, ""), nil
|
length, err := c.parseBuiltin(frame, []ssa.Value{expr.X}, "len")
|
||||||
|
if err != nil {
|
||||||
|
return llvm.Value{}, err // shouldn't happen
|
||||||
|
}
|
||||||
|
c.builder.CreateCall(c.mod.NamedFunction("runtime.lookupBoundsCheck"), []llvm.Value{length, index}, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup byte
|
||||||
|
buf := c.builder.CreateExtractValue(value, 1, "")
|
||||||
|
bufPtr := c.builder.CreateGEP(buf, []llvm.Value{index}, "")
|
||||||
|
return c.builder.CreateLoad(bufPtr, ""), nil
|
||||||
|
case *types.Map:
|
||||||
|
switch keyType := xType.Key().Underlying().(type) {
|
||||||
|
case *types.Basic:
|
||||||
|
if keyType.Kind() == types.String {
|
||||||
|
llvmValueType, err := c.getLLVMType(expr.Type())
|
||||||
|
if err != nil {
|
||||||
|
return llvm.Value{}, err
|
||||||
|
}
|
||||||
|
mapValueAlloca := c.builder.CreateAlloca(llvmValueType, "hashmap.value")
|
||||||
|
mapValuePtr := c.builder.CreateBitCast(mapValueAlloca, c.i8ptrType, "hashmap.valueptr")
|
||||||
|
params := []llvm.Value{value, index, mapValuePtr}
|
||||||
|
fn := c.mod.NamedFunction("runtime.hashmapGet")
|
||||||
|
c.builder.CreateCall(fn, params, "")
|
||||||
|
return c.builder.CreateLoad(mapValueAlloca, ""), nil
|
||||||
|
} else {
|
||||||
|
return llvm.Value{}, errors.New("todo: map lookup key type: " + keyType.String())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return llvm.Value{}, errors.New("todo: map lookup key type: " + keyType.String())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
panic("unknown lookup type: " + expr.String())
|
||||||
|
}
|
||||||
case *ssa.MakeInterface:
|
case *ssa.MakeInterface:
|
||||||
val, err := c.parseExpr(frame, expr.X)
|
val, err := c.parseExpr(frame, expr.X)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -1359,6 +1413,63 @@ func (c *Compiler) parseExpr(frame *Frame, expr ssa.Value) (llvm.Value, error) {
|
||||||
itf := llvm.ConstNamedStruct(c.mod.GetTypeByName("interface"), []llvm.Value{llvm.ConstInt(llvm.Int32Type(), uint64(itfTypeNum), false), llvm.Undef(c.i8ptrType)})
|
itf := llvm.ConstNamedStruct(c.mod.GetTypeByName("interface"), []llvm.Value{llvm.ConstInt(llvm.Int32Type(), uint64(itfTypeNum), false), llvm.Undef(c.i8ptrType)})
|
||||||
itf = c.builder.CreateInsertValue(itf, itfValue, 1, "")
|
itf = c.builder.CreateInsertValue(itf, itfValue, 1, "")
|
||||||
return itf, nil
|
return itf, nil
|
||||||
|
case *ssa.MakeMap:
|
||||||
|
mapType := expr.Type().Underlying().(*types.Map)
|
||||||
|
llvmKeyType, err := c.getLLVMType(mapType.Key().Underlying())
|
||||||
|
if err != nil {
|
||||||
|
return llvm.Value{}, err
|
||||||
|
}
|
||||||
|
llvmValueType, err := c.getLLVMType(mapType.Elem().Underlying())
|
||||||
|
if err != nil {
|
||||||
|
return llvm.Value{}, err
|
||||||
|
}
|
||||||
|
switch keyType := mapType.Key().Underlying().(type) {
|
||||||
|
case *types.Basic:
|
||||||
|
if keyType.Kind() == types.String {
|
||||||
|
// Create hashmap
|
||||||
|
llvmType := c.mod.GetTypeByName("runtime.hashmap")
|
||||||
|
size := llvm.ConstInt(c.uintptrType, c.targetData.TypeAllocSize(llvmType), false)
|
||||||
|
buf := c.builder.CreateCall(c.allocFunc, []llvm.Value{size}, "")
|
||||||
|
buf = c.builder.CreateBitCast(buf, llvm.PointerType(llvmType, 0), "")
|
||||||
|
|
||||||
|
// Set keySize
|
||||||
|
keySize := c.targetData.TypeAllocSize(llvmKeyType)
|
||||||
|
keyIndices := []llvm.Value{
|
||||||
|
llvm.ConstInt(llvm.Int32Type(), 0, false),
|
||||||
|
llvm.ConstInt(llvm.Int32Type(), 3, false), // keySize uint8
|
||||||
|
}
|
||||||
|
keySizePtr := c.builder.CreateGEP(buf, keyIndices, "hashmap.keySize")
|
||||||
|
c.builder.CreateStore(llvm.ConstInt(llvm.Int8Type(), keySize, false), keySizePtr)
|
||||||
|
|
||||||
|
// Set valueSize
|
||||||
|
valueSize := c.targetData.TypeAllocSize(llvmValueType)
|
||||||
|
valueIndices := []llvm.Value{
|
||||||
|
llvm.ConstInt(llvm.Int32Type(), 0, false),
|
||||||
|
llvm.ConstInt(llvm.Int32Type(), 4, false), // valueSize uint8
|
||||||
|
}
|
||||||
|
valueSizePtr := c.builder.CreateGEP(buf, valueIndices, "hashmap.valueSize")
|
||||||
|
c.builder.CreateStore(llvm.ConstInt(llvm.Int8Type(), valueSize, false), valueSizePtr)
|
||||||
|
|
||||||
|
// Create initial bucket
|
||||||
|
bucketType := c.mod.GetTypeByName("runtime.hashmapBucket")
|
||||||
|
bucketSize := c.targetData.TypeAllocSize(bucketType) + keySize*8 + valueSize*8
|
||||||
|
bucketSizeValue := llvm.ConstInt(c.uintptrType, bucketSize, false)
|
||||||
|
bucket := c.builder.CreateCall(c.allocFunc, []llvm.Value{bucketSizeValue}, "")
|
||||||
|
|
||||||
|
// Set initial bucket
|
||||||
|
bucketIndices := []llvm.Value{
|
||||||
|
llvm.ConstInt(llvm.Int32Type(), 0, false),
|
||||||
|
llvm.ConstInt(llvm.Int32Type(), 1, false), // buckets unsafe.Pointer
|
||||||
|
}
|
||||||
|
bucketsElementPtr := c.builder.CreateGEP(buf, bucketIndices, "hashmap.buckets")
|
||||||
|
c.builder.CreateStore(bucket, bucketsElementPtr)
|
||||||
|
return buf, nil
|
||||||
|
} else {
|
||||||
|
return llvm.Value{}, errors.New("todo: map key type: " + keyType.String())
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return llvm.Value{}, errors.New("todo: map key type: " + keyType.String())
|
||||||
|
}
|
||||||
case *ssa.Phi:
|
case *ssa.Phi:
|
||||||
t, err := c.getLLVMType(expr.Type())
|
t, err := c.getLLVMType(expr.Type())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -23,6 +23,9 @@ func main() {
|
||||||
println("sumrange(100) =", sumrange(100))
|
println("sumrange(100) =", sumrange(100))
|
||||||
println("strlen foo:", strlen("foo"))
|
println("strlen foo:", strlen("foo"))
|
||||||
|
|
||||||
|
m := map[string]int{"answer": 42, "foo": 3}
|
||||||
|
readMap(m, "answer")
|
||||||
|
|
||||||
foo := []int{1, 2, 4, 5}
|
foo := []int{1, 2, 4, 5}
|
||||||
println("len/cap foo:", len(foo), cap(foo))
|
println("len/cap foo:", len(foo), cap(foo))
|
||||||
println("foo[3]:", foo[3])
|
println("foo[3]:", foo[3])
|
||||||
|
@ -46,6 +49,10 @@ func runFunc(f func(int), arg int) {
|
||||||
f(arg)
|
f(arg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func readMap(m map[string]int, key string) {
|
||||||
|
println("map read:", key, "=", m[key])
|
||||||
|
}
|
||||||
|
|
||||||
func hello(n int) {
|
func hello(n int) {
|
||||||
println("hello from function pointer:", n)
|
println("hello from function pointer:", n)
|
||||||
}
|
}
|
||||||
|
|
133
src/runtime/hashmap.go
Обычный файл
133
src/runtime/hashmap.go
Обычный файл
|
@ -0,0 +1,133 @@
|
||||||
|
package runtime
|
||||||
|
|
||||||
|
// This is a hashmap implementation for the map[T]T type.
|
||||||
|
// It is very rougly based on the implementation of the Go hashmap:
|
||||||
|
//
|
||||||
|
// https://golang.org/src/runtime/hashmap.go
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The underlying hashmap structure for Go.
|
||||||
|
type hashmap struct {
|
||||||
|
next *hashmap // hashmap after evacuate (for iterators)
|
||||||
|
buckets unsafe.Pointer // pointer to array of buckets
|
||||||
|
count uint
|
||||||
|
keySize uint8 // maybe this can store the key type as well? E.g. keysize == 5 means string?
|
||||||
|
valueSize uint8
|
||||||
|
bucketBits uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// A hashmap bucket. A bucket is a container of 8 key/value pairs: first the
|
||||||
|
// following two entries, then the 8 keys, then the 8 values. This somewhat odd
|
||||||
|
// ordering is to make sure the keys and values are well aligned when one of
|
||||||
|
// them is smaller than the system word size.
|
||||||
|
type hashmapBucket struct {
|
||||||
|
tophash [8]uint8
|
||||||
|
next *hashmapBucket // next bucket (if there are more than 8 in a chain)
|
||||||
|
// Followed by the actual keys, and then the actual values. These are
|
||||||
|
// allocated but as they're of variable size they can't be shown here.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get FNV-1a hash of this string.
|
||||||
|
//
|
||||||
|
// https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash
|
||||||
|
func stringhash(s *string) uint32 {
|
||||||
|
var result uint32 = 2166136261 // FNV offset basis
|
||||||
|
for i := 0; i < len(*s); i++ {
|
||||||
|
result ^= uint32((*s)[i])
|
||||||
|
result *= 16777619 // FNV prime
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set a specified key to a given value. Grow the map if necessary.
|
||||||
|
func hashmapSet(m *hashmap, key string, value unsafe.Pointer) {
|
||||||
|
hash := stringhash(&key)
|
||||||
|
numBuckets := uintptr(1) << m.bucketBits
|
||||||
|
bucketNumber := (uintptr(hash) & (numBuckets - 1))
|
||||||
|
bucketSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8
|
||||||
|
bucketAddr := uintptr(m.buckets) + bucketSize*bucketNumber
|
||||||
|
bucket := (*hashmapBucket)(unsafe.Pointer(bucketAddr))
|
||||||
|
|
||||||
|
tophash := uint8(hash >> 24)
|
||||||
|
if tophash < 1 {
|
||||||
|
// 0 means empty slot, so make it bigger.
|
||||||
|
tophash += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// See whether the key already exists somewhere.
|
||||||
|
var emptySlotKey *string
|
||||||
|
var emptySlotValue unsafe.Pointer
|
||||||
|
var emptySlotTophash *byte
|
||||||
|
for bucket != nil {
|
||||||
|
for i := uintptr(0); i < 8; i++ {
|
||||||
|
slotKeyOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*uintptr(i)
|
||||||
|
slotKey := (*string)(unsafe.Pointer(bucketAddr + slotKeyOffset))
|
||||||
|
slotValueOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*uintptr(i)
|
||||||
|
slotValue := unsafe.Pointer(bucketAddr + slotValueOffset)
|
||||||
|
if bucket.tophash[i] == 0 && emptySlotKey == nil {
|
||||||
|
// Found an empty slot, store it for if we couldn't find an
|
||||||
|
// existing slot.
|
||||||
|
emptySlotKey = slotKey
|
||||||
|
emptySlotValue = slotValue
|
||||||
|
emptySlotTophash = &bucket.tophash[i]
|
||||||
|
}
|
||||||
|
if bucket.tophash[i] == tophash {
|
||||||
|
// Could be an existing value that's the same.
|
||||||
|
if key == *slotKey {
|
||||||
|
// found same key, replace it
|
||||||
|
memcpy(slotValue, value, uintptr(m.valueSize))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bucket = bucket.next
|
||||||
|
}
|
||||||
|
if emptySlotKey != nil {
|
||||||
|
*emptySlotKey = key
|
||||||
|
memcpy(emptySlotValue, value, uintptr(m.valueSize))
|
||||||
|
*emptySlotTophash = tophash
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic("todo: hashmap: grow bucket")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the value of a specified key, or zero the value if not found.
|
||||||
|
func hashmapGet(m *hashmap, key string, value unsafe.Pointer) {
|
||||||
|
hash := stringhash(&key)
|
||||||
|
numBuckets := uintptr(1) << m.bucketBits
|
||||||
|
bucketNumber := (uintptr(hash) & (numBuckets - 1))
|
||||||
|
bucketSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8
|
||||||
|
bucketAddr := uintptr(m.buckets) + bucketSize*bucketNumber
|
||||||
|
bucket := (*hashmapBucket)(unsafe.Pointer(bucketAddr))
|
||||||
|
|
||||||
|
tophash := uint8(hash >> 24)
|
||||||
|
if tophash < 1 {
|
||||||
|
// 0 means empty slot, so make it bigger.
|
||||||
|
tophash += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to find the key.
|
||||||
|
for bucket != nil {
|
||||||
|
for i := uintptr(0); i < 8; i++ {
|
||||||
|
slotKeyOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*uintptr(i)
|
||||||
|
slotKey := (*string)(unsafe.Pointer(bucketAddr + slotKeyOffset))
|
||||||
|
slotValueOffset := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*uintptr(i)
|
||||||
|
slotValue := unsafe.Pointer(bucketAddr + slotValueOffset)
|
||||||
|
if bucket.tophash[i] == tophash {
|
||||||
|
// This could be the key we're looking for.
|
||||||
|
if key == *slotKey {
|
||||||
|
// Found the key, copy it.
|
||||||
|
memcpy(value, slotValue, uintptr(m.valueSize))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bucket = bucket.next
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did not find the key.
|
||||||
|
memzero(value, uintptr(m.valueSize))
|
||||||
|
}
|
|
@ -1,5 +1,9 @@
|
||||||
package runtime
|
package runtime
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
const Compiler = "tgo"
|
const Compiler = "tgo"
|
||||||
|
|
||||||
// The bitness of the CPU (e.g. 8, 32, 64). Set by the compiler as a constant.
|
// The bitness of the CPU (e.g. 8, 32, 64). Set by the compiler as a constant.
|
||||||
|
@ -29,6 +33,20 @@ func stringequal(x, y string) bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Copy size bytes from src to dst. The memory areas must not overlap.
|
||||||
|
func memcpy(dst, src unsafe.Pointer, size uintptr) {
|
||||||
|
for i := uintptr(0); i < size; i++ {
|
||||||
|
*(*uint8)(unsafe.Pointer(uintptr(dst) + i)) = *(*uint8)(unsafe.Pointer(uintptr(src) + i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the given number of bytes to zero.
|
||||||
|
func memzero(ptr unsafe.Pointer, size uintptr) {
|
||||||
|
for i := uintptr(0); i < size; i++ {
|
||||||
|
*(*byte)(unsafe.Pointer(uintptr(ptr) + size)) = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func _panic(message interface{}) {
|
func _panic(message interface{}) {
|
||||||
printstring("panic: ")
|
printstring("panic: ")
|
||||||
printitf(message)
|
printitf(message)
|
||||||
|
|
Загрузка…
Создание таблицы
Сослаться в новой задаче