ssa: add llvm debug info
This commit is contained in:
36
ssa/decl.go
36
ssa/decl.go
@@ -17,6 +17,7 @@
|
||||
package ssa
|
||||
|
||||
import (
|
||||
"go/token"
|
||||
"go/types"
|
||||
"log"
|
||||
"strconv"
|
||||
@@ -173,6 +174,8 @@ type aFunction struct {
|
||||
freeVars Expr
|
||||
base int // base = 1 if hasFreeVars; base = 0 otherwise
|
||||
hasVArg bool
|
||||
|
||||
diFunc DIFunction
|
||||
}
|
||||
|
||||
// Function represents a function or method.
|
||||
@@ -274,6 +277,10 @@ func (p Function) NewBuilder() Builder {
|
||||
return &aBuilder{b, nil, p, p.Pkg, prog}
|
||||
}
|
||||
|
||||
func (p Function) NewDIBuilder() *llvm.DIBuilder {
|
||||
return llvm.NewDIBuilder(p.Pkg.mod)
|
||||
}
|
||||
|
||||
// HasBody reports whether the function has a body.
|
||||
func (p Function) HasBody() bool {
|
||||
return len(p.blks) > 0
|
||||
@@ -324,4 +331,33 @@ func (p Function) SetRecover(blk BasicBlock) {
|
||||
p.recov = blk
|
||||
}
|
||||
|
||||
func (p Function) scopeMeta(b diBuilder, pos token.Position) DIScopeMeta {
|
||||
if p.diFunc == nil {
|
||||
paramTypes := make([]llvm.Metadata, len(p.params))
|
||||
for i, t := range p.params {
|
||||
paramTypes[i] = b.DIType(t, pos).ll
|
||||
}
|
||||
diFuncType := b.di.CreateSubroutineType(llvm.DISubroutineType{
|
||||
File: b.DIFile(pos.Filename).ll,
|
||||
Parameters: paramTypes,
|
||||
})
|
||||
p.diFunc = &aDIFunction{
|
||||
b.di.CreateFunction(
|
||||
p.Pkg.cu.ll,
|
||||
llvm.DIFunction{
|
||||
Type: diFuncType,
|
||||
Name: p.Name(),
|
||||
LinkageName: p.Name(),
|
||||
File: b.DIFile(pos.Filename).ll,
|
||||
Line: pos.Line,
|
||||
IsDefinition: true,
|
||||
Optimized: false,
|
||||
},
|
||||
),
|
||||
}
|
||||
p.impl.SetSubprogram(p.diFunc.ll)
|
||||
}
|
||||
return &aDIScopeMeta{p.diFunc.ll}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
440
ssa/di.go
Normal file
440
ssa/di.go
Normal file
@@ -0,0 +1,440 @@
|
||||
package ssa
|
||||
|
||||
import (
|
||||
"debug/dwarf"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/goplus/llvm"
|
||||
)
|
||||
|
||||
type aDIBuilder struct {
|
||||
di *llvm.DIBuilder
|
||||
prog Program
|
||||
diTypes map[Type]DIType
|
||||
}
|
||||
|
||||
type diBuilder = *aDIBuilder
|
||||
|
||||
func newDIBuilder(prog Program, m llvm.Module) diBuilder {
|
||||
ctx := m.Context()
|
||||
m.AddNamedMetadataOperand("llvm.module.flags",
|
||||
ctx.MDNode([]llvm.Metadata{
|
||||
llvm.ConstInt(ctx.Int32Type(), 2, false).ConstantAsMetadata(), // Warning on mismatch
|
||||
ctx.MDString("Debug Info Version"),
|
||||
llvm.ConstInt(ctx.Int32Type(), 3, false).ConstantAsMetadata(),
|
||||
}),
|
||||
)
|
||||
m.AddNamedMetadataOperand("llvm.module.flags",
|
||||
ctx.MDNode([]llvm.Metadata{
|
||||
llvm.ConstInt(ctx.Int32Type(), 7, false).ConstantAsMetadata(), // Max on mismatch
|
||||
ctx.MDString("Dwarf Version"),
|
||||
llvm.ConstInt(ctx.Int32Type(), 5, false).ConstantAsMetadata(),
|
||||
}),
|
||||
)
|
||||
return &aDIBuilder{
|
||||
di: llvm.NewDIBuilder(m),
|
||||
prog: prog,
|
||||
diTypes: make(map[*aType]DIType),
|
||||
}
|
||||
}
|
||||
|
||||
func (b diBuilder) Finalize() {
|
||||
b.di.Finalize()
|
||||
b.di.Destroy()
|
||||
b.di = nil
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type aCompilationUnit struct {
|
||||
ll llvm.Metadata
|
||||
}
|
||||
|
||||
type CompilationUnit = *aCompilationUnit
|
||||
|
||||
func (b diBuilder) CreateCompileUnit(filename, dir string) CompilationUnit {
|
||||
return &aCompilationUnit{ll: b.di.CreateCompileUnit(llvm.DICompileUnit{
|
||||
Language: llvm.DW_LANG_Go,
|
||||
File: filename,
|
||||
Dir: dir,
|
||||
Producer: "LLGo",
|
||||
Optimized: true,
|
||||
RuntimeVersion: 1,
|
||||
})}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type aDIScopeMeta struct {
|
||||
ll llvm.Metadata
|
||||
}
|
||||
|
||||
type DIScopeMeta = *aDIScopeMeta
|
||||
|
||||
type DIScope interface {
|
||||
scopeMeta(b diBuilder, pos token.Position) DIScopeMeta
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type aDIFile struct {
|
||||
ll llvm.Metadata
|
||||
}
|
||||
|
||||
type DIFile = *aDIFile
|
||||
|
||||
func (b diBuilder) createFile(filename string) DIFile {
|
||||
dir, file := filepath.Split(filename)
|
||||
return &aDIFile{ll: b.di.CreateFile(file, dir)}
|
||||
}
|
||||
|
||||
func (f DIFile) scopeMeta(b diBuilder, cu CompilationUnit, pos token.Position) DIScopeMeta {
|
||||
return &aDIScopeMeta{b.DIFile(pos.Filename).ll}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type aDILexicalBlock struct {
|
||||
ll llvm.Metadata
|
||||
}
|
||||
|
||||
type DILexicalBlock = *aDILexicalBlock
|
||||
|
||||
func (b diBuilder) createLexicalBlock(scope DIScope, pos token.Position) DILexicalBlock {
|
||||
block := llvm.DILexicalBlock{
|
||||
File: b.DIFile(pos.Filename).ll,
|
||||
Line: pos.Line,
|
||||
Column: pos.Column,
|
||||
}
|
||||
return &aDILexicalBlock{ll: b.di.CreateLexicalBlock(scope.scopeMeta(b, pos).ll, block)}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type aDIType struct {
|
||||
ll llvm.Metadata
|
||||
}
|
||||
|
||||
type DIType = *aDIType
|
||||
|
||||
func (b diBuilder) createType(ty Type, pos token.Position) DIType {
|
||||
fmt.Printf("Create type: %T, %v\n", ty.RawType(), ty.RawType())
|
||||
var typ llvm.Metadata
|
||||
switch t := ty.RawType().(type) {
|
||||
case *types.Basic:
|
||||
if t.Kind() == types.UnsafePointer {
|
||||
typ = b.di.CreatePointerType(llvm.DIPointerType{
|
||||
Name: "unsafe.Pointer",
|
||||
SizeInBits: b.prog.SizeOf(b.prog.rawType(t)) * 8,
|
||||
AlignInBits: uint32(b.prog.sizes.Alignof(t) * 8),
|
||||
AddressSpace: 0,
|
||||
})
|
||||
return &aDIType{typ}
|
||||
}
|
||||
|
||||
var encoding llvm.DwarfTypeEncoding
|
||||
if t.Info()&types.IsBoolean != 0 {
|
||||
encoding = llvm.DW_ATE_boolean
|
||||
} else if t.Info()&types.IsUnsigned != 0 {
|
||||
encoding = llvm.DW_ATE_unsigned
|
||||
} else if t.Info()&types.IsInteger != 0 {
|
||||
encoding = llvm.DW_ATE_signed
|
||||
} else if t.Info()&types.IsFloat != 0 {
|
||||
encoding = llvm.DW_ATE_float
|
||||
} else if t.Info()&types.IsComplex != 0 {
|
||||
encoding = llvm.DW_ATE_complex_float
|
||||
} else if t.Info()&types.IsString != 0 {
|
||||
typ = b.di.CreateBasicType(llvm.DIBasicType{
|
||||
Name: "string",
|
||||
SizeInBits: b.prog.SizeOf(b.prog.rawType(t)) * 8,
|
||||
Encoding: llvm.DW_ATE_unsigned_char,
|
||||
})
|
||||
return &aDIType{typ}
|
||||
} else {
|
||||
encoding = llvm.DW_ATE_unsigned
|
||||
panic("todo: basic type")
|
||||
}
|
||||
|
||||
typ = b.di.CreateBasicType(llvm.DIBasicType{
|
||||
Name: t.Name(),
|
||||
SizeInBits: b.prog.SizeOf(b.prog.rawType(t)) * 8,
|
||||
Encoding: encoding,
|
||||
})
|
||||
case *types.Pointer:
|
||||
return b.createPointerType(b.prog.rawType(t.Elem()), pos)
|
||||
case *types.Named:
|
||||
return b.DIType(b.prog.rawType(t.Underlying()), pos)
|
||||
case *types.Interface:
|
||||
return b.createBasicType(ty)
|
||||
case *types.Slice:
|
||||
return b.createBasicType(ty)
|
||||
case *types.Struct:
|
||||
return b.createStructType(ty, pos)
|
||||
case *types.Signature:
|
||||
return b.createFuncPtrType(b.prog.rawType(t), pos)
|
||||
case *types.Tuple:
|
||||
return b.createBasicType(ty)
|
||||
case *types.Array:
|
||||
return b.createBasicType(ty)
|
||||
default:
|
||||
panic(fmt.Errorf("can't create debug info of type: %v, %T", ty.RawType(), ty.RawType()))
|
||||
}
|
||||
return &aDIType{typ}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type aDIFunction struct {
|
||||
ll llvm.Metadata
|
||||
}
|
||||
|
||||
type DIFunction = *aDIFunction
|
||||
|
||||
func (b diBuilder) CreateFunction(scope DIScope, pos token.Position, name, linkageName string, ty DIType, isLocalToUnit, isDefinition, isOptimized bool) DIFunction {
|
||||
return &aDIFunction{ll: scope.scopeMeta(b, pos).ll}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type aDIGlobalVariableExpression struct {
|
||||
ll llvm.Metadata
|
||||
}
|
||||
|
||||
type DIGlobalVariableExpression = *aDIGlobalVariableExpression
|
||||
|
||||
func (b diBuilder) CreateGlobalVariableExpression(scope DIScope, pos token.Position, name, linkageName string, ty DIType, isLocalToUnit bool) DIGlobalVariableExpression {
|
||||
return &aDIGlobalVariableExpression{
|
||||
ll: b.di.CreateGlobalVariableExpression(
|
||||
scope.scopeMeta(b, pos).ll,
|
||||
llvm.DIGlobalVariableExpression{
|
||||
Name: name,
|
||||
LinkageName: linkageName,
|
||||
File: b.DIFile(pos.Filename).ll,
|
||||
Line: pos.Line,
|
||||
Type: ty.ll,
|
||||
LocalToUnit: isLocalToUnit,
|
||||
// TODO(lijie): check the following fields
|
||||
// Expr: llvm.Metadata{},
|
||||
// Decl: llvm.Metadata{},
|
||||
// AlignInBits: 0,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
type aDIVar struct {
|
||||
ll llvm.Metadata
|
||||
}
|
||||
|
||||
type DIVar = *aDIVar
|
||||
|
||||
func (b diBuilder) CreateParameterVariable(scope DIScope, pos token.Position, name string, argNo int, ty DIType) DIVar {
|
||||
return &aDIVar{
|
||||
ll: b.di.CreateParameterVariable(
|
||||
scope.scopeMeta(b, pos).ll,
|
||||
llvm.DIParameterVariable{
|
||||
Name: name,
|
||||
File: b.DIFile(pos.Filename).ll,
|
||||
Line: pos.Line,
|
||||
ArgNo: argNo,
|
||||
Type: ty.ll,
|
||||
AlwaysPreserve: true,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (b diBuilder) CreateAutoVariable(scope DIScope, pos token.Position, name string, ty DIType) DIVar {
|
||||
return &aDIVar{
|
||||
ll: b.di.CreateAutoVariable(
|
||||
scope.scopeMeta(b, pos).ll,
|
||||
llvm.DIAutoVariable{
|
||||
Name: name,
|
||||
File: b.DIFile(pos.Filename).ll,
|
||||
Line: pos.Line,
|
||||
Type: ty.ll,
|
||||
AlwaysPreserve: true,
|
||||
},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (b diBuilder) createBasicType(t Type) DIType {
|
||||
return &aDIType{ll: b.di.CreateBasicType(llvm.DIBasicType{
|
||||
Name: t.RawType().String(),
|
||||
SizeInBits: b.prog.SizeOf(t) * 8,
|
||||
Encoding: llvm.DW_ATE_unsigned,
|
||||
})}
|
||||
}
|
||||
|
||||
func (b diBuilder) createPointerType(ty Type, pos token.Position) DIType {
|
||||
return &aDIType{ll: b.di.CreatePointerType(llvm.DIPointerType{
|
||||
Pointee: b.DIType(ty, pos).ll,
|
||||
SizeInBits: b.prog.SizeOf(ty) * 8,
|
||||
AlignInBits: uint32(b.prog.sizes.Alignof(ty.RawType())) * 8,
|
||||
AddressSpace: 0,
|
||||
})}
|
||||
}
|
||||
|
||||
func (b diBuilder) createStructType(ty Type, pos token.Position) (ret DIType) {
|
||||
scope := b.DIFile(pos.Filename)
|
||||
ret = &aDIType{b.di.CreateReplaceableCompositeType(
|
||||
scope.ll,
|
||||
llvm.DIReplaceableCompositeType{
|
||||
Tag: dwarf.TagStructType,
|
||||
Name: ty.RawType().String(),
|
||||
},
|
||||
)}
|
||||
b.diTypes[ty] = ret
|
||||
|
||||
// Create struct type
|
||||
structType := ty.RawType().(*types.Struct)
|
||||
fields := make([]llvm.Metadata, structType.NumFields())
|
||||
|
||||
for i := 0; i < structType.NumFields(); i++ {
|
||||
field := structType.Field(i)
|
||||
fields[i] = b.di.CreateMemberType(
|
||||
scope.ll,
|
||||
llvm.DIMemberType{
|
||||
Name: field.Name(),
|
||||
File: b.DIFile(pos.Filename).ll,
|
||||
Line: pos.Line,
|
||||
SizeInBits: b.prog.SizeOf(b.prog.rawType(field.Type())) * 8,
|
||||
AlignInBits: 8,
|
||||
OffsetInBits: b.prog.OffsetOf(ty, i) * 8,
|
||||
Type: b.DIType(b.prog.rawType(field.Type()), pos).ll,
|
||||
},
|
||||
)
|
||||
}
|
||||
st := b.di.CreateStructType(
|
||||
scope.ll,
|
||||
llvm.DIStructType{
|
||||
Name: ty.RawType().String(),
|
||||
File: b.DIFile(pos.Filename).ll,
|
||||
Line: pos.Line,
|
||||
SizeInBits: b.prog.SizeOf(ty) * 8,
|
||||
AlignInBits: uint32(b.prog.sizes.Alignof(structType) * 8),
|
||||
Elements: fields,
|
||||
},
|
||||
)
|
||||
ret.ll.ReplaceAllUsesWith(st)
|
||||
ret.ll = st
|
||||
return
|
||||
}
|
||||
|
||||
func (b diBuilder) createFuncPtrType(ty Type, pos token.Position) DIType {
|
||||
sig := ty.RawType().(*types.Signature)
|
||||
retTy := b.DIType(b.prog.rawType(sig.Results()), pos)
|
||||
paramTys := make([]DIType, sig.Params().Len())
|
||||
for i := 0; i < sig.Params().Len(); i++ {
|
||||
paramTys[i] = b.DIType(b.prog.rawType(sig.Params().At(i).Type()), pos)
|
||||
}
|
||||
rt := b.createSubroutineType(b.DIFile(pos.Filename), retTy, paramTys)
|
||||
return &aDIType{ll: b.di.CreatePointerType(llvm.DIPointerType{
|
||||
Pointee: rt.ll,
|
||||
SizeInBits: b.prog.SizeOf(ty) * 8,
|
||||
AlignInBits: 8,
|
||||
AddressSpace: 0,
|
||||
})}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (b diBuilder) createSubroutineType(file DIFile, retTy DIType, paramTys []DIType) DIType {
|
||||
params := make([]llvm.Metadata, len(paramTys)+1)
|
||||
params[0] = retTy.ll
|
||||
for i, ty := range paramTys {
|
||||
params[i+1] = ty.ll
|
||||
}
|
||||
return &aDIType{ll: b.di.CreateSubroutineType(llvm.DISubroutineType{
|
||||
File: file.ll,
|
||||
Parameters: params,
|
||||
Flags: 0,
|
||||
})}
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
func (b diBuilder) Debug(v Expr, dv DIVar, scope DIScope, pos token.Position, blk BasicBlock) {
|
||||
loc := llvm.DebugLoc{
|
||||
Line: uint(pos.Line),
|
||||
Col: uint(pos.Column),
|
||||
Scope: scope.scopeMeta(b, pos).ll,
|
||||
}
|
||||
b.di.InsertDeclareAtEnd(
|
||||
v.impl,
|
||||
dv.ll,
|
||||
b.di.CreateExpression(nil),
|
||||
loc,
|
||||
blk.last,
|
||||
)
|
||||
}
|
||||
|
||||
func (b diBuilder) DebugValue(v Expr, dv DIVar, scope DIScope, pos token.Position, blk BasicBlock) {
|
||||
loc := llvm.DebugLoc{
|
||||
Line: uint(pos.Line),
|
||||
Col: uint(pos.Column),
|
||||
Scope: scope.scopeMeta(b, pos).ll,
|
||||
}
|
||||
b.di.InsertValueAtEnd(
|
||||
v.impl,
|
||||
dv.ll,
|
||||
b.di.CreateExpression(nil),
|
||||
loc,
|
||||
blk.last,
|
||||
)
|
||||
}
|
||||
|
||||
func (b diBuilder) DIType(t Type, pos token.Position) DIType {
|
||||
if ty, ok := b.diTypes[t]; ok {
|
||||
return ty
|
||||
}
|
||||
ty := b.createType(t, pos)
|
||||
b.diTypes[t] = ty
|
||||
return ty
|
||||
}
|
||||
|
||||
func (b diBuilder) DIVarParam(f Function, pos token.Position, varName string, vt DIType, argNo int) DIVar {
|
||||
return b.CreateParameterVariable(
|
||||
f,
|
||||
pos,
|
||||
varName,
|
||||
argNo,
|
||||
vt,
|
||||
)
|
||||
}
|
||||
|
||||
func (b diBuilder) DIVarAuto(f Function, pos token.Position, varName string, vt DIType) DIVar {
|
||||
return b.CreateAutoVariable(
|
||||
f,
|
||||
pos,
|
||||
varName,
|
||||
vt,
|
||||
)
|
||||
}
|
||||
|
||||
func (b diBuilder) DIFile(filename string) DIFile {
|
||||
return b.createFile(filename)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func (b Builder) SetCurrentDebugLocation(f Function, pos token.Position) {
|
||||
b.impl.SetCurrentDebugLocation(
|
||||
uint(pos.Line),
|
||||
uint(pos.Column),
|
||||
f.scopeMeta(b.Pkg.DIBuilder(), pos).ll,
|
||||
f.impl.InstructionDebugLoc(),
|
||||
)
|
||||
}
|
||||
func (b diBuilder) DebugFunction(f Function, pos token.Position) {
|
||||
// attach debug info to function
|
||||
f.scopeMeta(b, pos)
|
||||
}
|
||||
@@ -338,6 +338,8 @@ func (p Program) tyComplex128() llvm.Type {
|
||||
// NewPackage creates a new package.
|
||||
func (p Program) NewPackage(name, pkgPath string) Package {
|
||||
mod := p.ctx.NewModule(pkgPath)
|
||||
di := newDIBuilder(p, mod)
|
||||
cu := di.CreateCompileUnit(name, pkgPath)
|
||||
// TODO(xsw): Finalize may cause panic, so comment it.
|
||||
// mod.Finalize()
|
||||
gbls := make(map[string]Global)
|
||||
@@ -352,7 +354,7 @@ func (p Program) NewPackage(name, pkgPath string) Package {
|
||||
// p.needPyInit = false
|
||||
ret := &aPackage{
|
||||
mod: mod, vars: gbls, fns: fns, stubs: stubs,
|
||||
pyobjs: pyobjs, pymods: pymods, strs: strs, named: named, Prog: p}
|
||||
pyobjs: pyobjs, pymods: pymods, strs: strs, named: named, Prog: p, di: di, cu: cu}
|
||||
ret.abi.Init(pkgPath)
|
||||
return ret
|
||||
}
|
||||
@@ -589,6 +591,8 @@ type aPackage struct {
|
||||
abi abi.Builder
|
||||
|
||||
Prog Program
|
||||
di diBuilder
|
||||
cu CompilationUnit
|
||||
|
||||
vars map[string]Global
|
||||
fns map[string]Function
|
||||
@@ -706,6 +710,14 @@ func (p Package) AfterInit(b Builder, ret BasicBlock) {
|
||||
}
|
||||
}
|
||||
|
||||
func (p Package) Finalize() {
|
||||
p.di.Finalize()
|
||||
}
|
||||
|
||||
func (p Package) DIBuilder() diBuilder {
|
||||
return p.di
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
/*
|
||||
|
||||
@@ -287,6 +287,24 @@ func (b Builder) Times(n Expr, loop func(i Expr)) {
|
||||
b.blk.last = blks[2].last
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func (b Builder) Debug(v Expr, dv DIVar, scope DIScope, pos token.Position) {
|
||||
b.Pkg.DIBuilder().Debug(v, dv, scope, pos, b.blk)
|
||||
}
|
||||
|
||||
func (b Builder) DebugValue(v Expr, dv DIVar, scope DIScope, pos token.Position) {
|
||||
b.Pkg.DIBuilder().DebugValue(v, dv, scope, pos, b.blk)
|
||||
}
|
||||
|
||||
func (b Builder) DIVarParam(f Function, pos token.Position, varName string, vt DIType, argNo int) DIVar {
|
||||
return b.Pkg.DIBuilder().DIVarParam(f, pos, varName, vt, argNo)
|
||||
}
|
||||
|
||||
func (b Builder) DIVarAuto(f Function, pos token.Position, varName string, vt DIType) DIVar {
|
||||
return b.Pkg.DIBuilder().DIVarAuto(f, pos, varName, vt)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
/*
|
||||
type caseStmt struct {
|
||||
|
||||
Reference in New Issue
Block a user