diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt index e819aea3b..15621a3e1 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/AsmGen.kt @@ -2881,4 +2881,108 @@ $repeatLabel lda $counterVar } } + internal fun popCpuStack(dt: DataType, target: VarDecl, scope: Subroutine?) { + // note: because A is pushed first so popped last, saving A is often not required here. + val parameter = target.subroutineParameter + if(parameter!=null) { + val sub = parameter.definingSubroutine!! + require(sub.isAsmSubroutine) { "push/pop arg passing only supported on asmsubs" } + val shouldKeepA = sub.asmParameterRegisters.any { it.registerOrPair==RegisterOrPair.AX || it.registerOrPair==RegisterOrPair.AY } + val reg = sub.asmParameterRegisters[sub.parameters.indexOf(parameter)] + if(reg.statusflag!=null) { + if(shouldKeepA) + out(" sta P8ZP_SCRATCH_REG") + out(""" + clc + pla + beq + + sec ++""") + if(shouldKeepA) + out(" lda P8ZP_SCRATCH_REG") + } + else { + if (dt in ByteDatatypes) { + if (isTargetCpu(CpuType.CPU65c02)) { + when (reg.registerOrPair) { + RegisterOrPair.A -> out(" pla") + RegisterOrPair.X -> out(" plx") + RegisterOrPair.Y -> out(" ply") + in Cx16VirtualRegisters -> out(" pla | sta cx16.${reg.registerOrPair!!.name.lowercase()}") + else -> throw AssemblyError("invalid target register ${reg.registerOrPair}") + } + } else { + when (reg.registerOrPair) { + RegisterOrPair.A -> out(" pla") + RegisterOrPair.X -> { + if(shouldKeepA) + out(" sta P8ZP_SCRATCH_REG | pla | tax | lda P8ZP_SCRATCH_REG") + else + out(" pla | tax") + } + RegisterOrPair.Y -> { + if(shouldKeepA) + out(" sta P8ZP_SCRATCH_REG | pla | tay | lda P8ZP_SCRATCH_REG") + else + out(" pla | tay") + } + in Cx16VirtualRegisters -> out(" pla | sta cx16.${reg.registerOrPair!!.name.lowercase()}") + else -> throw AssemblyError("invalid target register ${reg.registerOrPair}") + } + } + } else { + // word pop + if (isTargetCpu(CpuType.CPU65c02)) + when (reg.registerOrPair) { + RegisterOrPair.AX -> out(" plx | pla") + RegisterOrPair.AY -> out(" ply | pla") + RegisterOrPair.XY -> out(" ply | plx") + in Cx16VirtualRegisters -> { + val regname = reg.registerOrPair!!.name.lowercase() + out(" pla | sta cx16.$regname+1 | pla | sta cx16.$regname") + } + else -> throw AssemblyError("invalid target register ${reg.registerOrPair}") + } + else { + when (reg.registerOrPair) { + RegisterOrPair.AX -> out(" pla | tax | pla") + RegisterOrPair.AY -> out(" pla | tay | pla") + RegisterOrPair.XY -> out(" pla | tay | pla | tax") + in Cx16VirtualRegisters -> { + val regname = reg.registerOrPair!!.name.lowercase() + out(" pla | sta cx16.$regname+1 | pla | sta cx16.$regname") + } + else -> throw AssemblyError("invalid target register ${reg.registerOrPair}") + } + } + } + } + } else { + val tgt = AsmAssignTarget(TargetStorageKind.VARIABLE, program, this, target.datatype, scope, variableAsmName = asmVariableName(target.name)) + if (dt in ByteDatatypes) { + out(" pla") + assignRegister(RegisterOrPair.A, tgt) + } else { + if (isTargetCpu(CpuType.CPU65c02)) + out(" ply | pla") + else + out(" pla | tay | pla") + assignRegister(RegisterOrPair.AY, tgt) + } + } + } + + internal fun pushCpuStack(dt: DataType, value: Expression) { + val signed = value.inferType(program).oneOf(DataType.BYTE, DataType.WORD) + if(dt in ByteDatatypes) { + assignExpressionToRegister(value, RegisterOrPair.A, signed) + out(" pha") + } else { + assignExpressionToRegister(value, RegisterOrPair.AY, signed) + if (isTargetCpu(CpuType.CPU65c02)) + out(" pha | phy") + else + out(" pha | tya | pha") + } + } } diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/BuiltinFunctionsAsmGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/BuiltinFunctionsAsmGen.kt index 68f9902b4..93399eb90 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/BuiltinFunctionsAsmGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/BuiltinFunctionsAsmGen.kt @@ -102,8 +102,20 @@ internal class BuiltinFunctionsAsmGen(private val program: Program, "pokew" -> funcPokeW(fcall) "pokemon" -> { /* meme function */ } "poke" -> throw AssemblyError("poke() should have been replaced by @()") - "push", "pushw" -> funcPush(fcall, func) - "pop", "popw" -> funcPop(fcall, func) + "push" -> asmgen.pushCpuStack(DataType.UBYTE, fcall.args[0]) + "pushw" -> asmgen.pushCpuStack(DataType.UWORD, fcall.args[0]) + "pop" -> { + require(fcall.args[0] is IdentifierReference) { + "attempt to pop a value into a differently typed variable, or in something else that isn't supported ${(fcall as Node).position}" + } + asmgen.popCpuStack(DataType.UBYTE, (fcall.args[0] as IdentifierReference).targetVarDecl(program)!!, (fcall as Node).definingSubroutine) + } + "popw" -> { + require(fcall.args[0] is IdentifierReference) { + "attempt to pop a value into a differently typed variable, or in something else that isn't supported ${(fcall as Node).position}" + } + asmgen.popCpuStack(DataType.UWORD, (fcall.args[0] as IdentifierReference).targetVarDecl(program)!!, (fcall as Node).definingSubroutine) + } "rsave" -> funcRsave() "rsavex" -> funcRsaveX() "rrestore" -> funcRrestore() @@ -166,117 +178,6 @@ internal class BuiltinFunctionsAsmGen(private val program: Program, asmgen.out(" sta P8ZP_SCRATCH_B1 | pla | tax | lda P8ZP_SCRATCH_B1") } - private fun funcPop(fcall: IFunctionCall, func: FSignature) { - // note: because A is pushed first so popped last, saving A is often not required here. - require(fcall.args[0] is IdentifierReference) { - "attempt to pop a value into a differently typed variable, or in something else that isn't supported ${(fcall as Node).position}" - } - val target = (fcall.args[0] as IdentifierReference).targetVarDecl(program)!! - val parameter = target.subroutineParameter - if(parameter!=null) { - val sub = parameter.definingSubroutine!! - require(sub.isAsmSubroutine) { - "push/pop arg passing only supported on asmsubs ${(fcall as Node).position}" - } - val shouldKeepA = sub.asmParameterRegisters.any { it.registerOrPair==RegisterOrPair.AX || it.registerOrPair==RegisterOrPair.AY } - val reg = sub.asmParameterRegisters[sub.parameters.indexOf(parameter)] - if(reg.statusflag!=null) { - if(shouldKeepA) - asmgen.out(" sta P8ZP_SCRATCH_REG") - asmgen.out(""" - clc - pla - beq + - sec -+""") - if(shouldKeepA) - asmgen.out(" lda P8ZP_SCRATCH_REG") - } - else { - if (func.name == "pop") { - if (asmgen.isTargetCpu(CpuType.CPU65c02)) { - when (reg.registerOrPair) { - RegisterOrPair.A -> asmgen.out(" pla") - RegisterOrPair.X -> asmgen.out(" plx") - RegisterOrPair.Y -> asmgen.out(" ply") - in Cx16VirtualRegisters -> asmgen.out(" pla | sta cx16.${reg.registerOrPair!!.name.lowercase()}") - else -> throw AssemblyError("invalid target register ${reg.registerOrPair}") - } - } else { - when (reg.registerOrPair) { - RegisterOrPair.A -> asmgen.out(" pla") - RegisterOrPair.X -> { - if(shouldKeepA) - asmgen.out(" sta P8ZP_SCRATCH_REG | pla | tax | lda P8ZP_SCRATCH_REG") - else - asmgen.out(" pla | tax") - } - RegisterOrPair.Y -> { - if(shouldKeepA) - asmgen.out(" sta P8ZP_SCRATCH_REG | pla | tay | lda P8ZP_SCRATCH_REG") - else - asmgen.out(" pla | tay") - } - in Cx16VirtualRegisters -> asmgen.out(" pla | sta cx16.${reg.registerOrPair!!.name.lowercase()}") - else -> throw AssemblyError("invalid target register ${reg.registerOrPair}") - } - } - } else { - // word pop - if (asmgen.isTargetCpu(CpuType.CPU65c02)) - when (reg.registerOrPair) { - RegisterOrPair.AX -> asmgen.out(" plx | pla") - RegisterOrPair.AY -> asmgen.out(" ply | pla") - RegisterOrPair.XY -> asmgen.out(" ply | plx") - in Cx16VirtualRegisters -> { - val regname = reg.registerOrPair!!.name.lowercase() - asmgen.out(" pla | sta cx16.$regname+1 | pla | sta cx16.$regname") - } - else -> throw AssemblyError("invalid target register ${reg.registerOrPair}") - } - else { - when (reg.registerOrPair) { - RegisterOrPair.AX -> asmgen.out(" pla | tax | pla") - RegisterOrPair.AY -> asmgen.out(" pla | tay | pla") - RegisterOrPair.XY -> asmgen.out(" pla | tay | pla | tax") - in Cx16VirtualRegisters -> { - val regname = reg.registerOrPair!!.name.lowercase() - asmgen.out(" pla | sta cx16.$regname+1 | pla | sta cx16.$regname") - } - else -> throw AssemblyError("invalid target register ${reg.registerOrPair}") - } - } - } - } - } else { - val tgt = AsmAssignTarget(TargetStorageKind.VARIABLE, program, asmgen, target.datatype, (fcall as Node).definingSubroutine, variableAsmName = asmgen.asmVariableName(target.name)) - if (func.name == "pop") { - asmgen.out(" pla") - asmgen.assignRegister(RegisterOrPair.A, tgt) - } else { - if (asmgen.isTargetCpu(CpuType.CPU65c02)) - asmgen.out(" ply | pla") - else - asmgen.out(" pla | tay | pla") - asmgen.assignRegister(RegisterOrPair.AY, tgt) - } - } - } - - private fun funcPush(fcall: IFunctionCall, func: FSignature) { - val signed = fcall.args[0].inferType(program).oneOf(DataType.BYTE, DataType.WORD) - if(func.name=="push") { - asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.A, signed) - asmgen.out(" pha") - } else { - asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.AY, signed) - if (asmgen.isTargetCpu(CpuType.CPU65c02)) - asmgen.out(" pha | phy") - else - asmgen.out(" pha | tya | pha") - } - } - private fun funcCallFar(fcall: IFunctionCall) { if(asmgen.options.compTarget.name != "cx16") throw AssemblyError("callfar only works on cx16 target at this time") diff --git a/codeGenCpu6502/src/prog8/codegen/cpu6502/FunctionCallAsmGen.kt b/codeGenCpu6502/src/prog8/codegen/cpu6502/FunctionCallAsmGen.kt index 3815db601..a37ccc1fa 100644 --- a/codeGenCpu6502/src/prog8/codegen/cpu6502/FunctionCallAsmGen.kt +++ b/codeGenCpu6502/src/prog8/codegen/cpu6502/FunctionCallAsmGen.kt @@ -14,7 +14,6 @@ import prog8.codegen.cpu6502.assignment.AsmAssignTarget import prog8.codegen.cpu6502.assignment.AsmAssignment import prog8.codegen.cpu6502.assignment.TargetStorageKind import prog8.compilerinterface.AssemblyError -import prog8.compilerinterface.CpuType internal class FunctionCallAsmGen(private val program: Program, private val asmgen: AsmGen) { @@ -193,7 +192,7 @@ internal class FunctionCallAsmGen(private val program: Program, private val asmg argumentViaRegister(sub, IndexedValue(0, sub.parameters.single()), call.args[0]) } else { if(asmgen.asmsubArgsHaveRegisterClobberRisk(call.args, sub.asmParameterRegisters)) { - registerArgsViaStackEvaluation(call, sub) + registerArgsViaCpuStackEvaluation(call, sub) } else { asmgen.asmsubArgsEvalOrder(sub).forEach { val param = sub.parameters[it] @@ -204,121 +203,24 @@ internal class FunctionCallAsmGen(private val program: Program, private val asmg } } - private fun registerArgsViaStackEvaluation(call: IFunctionCall, callee: Subroutine) { + private fun registerArgsViaCpuStackEvaluation(call: IFunctionCall, callee: Subroutine) { // this is called when one or more of the arguments are 'complex' and // cannot be assigned to a register easily or risk clobbering other registers. - // TODO find another way to prepare the arguments, without using the eval stack: use a few temporary variables instead, - // or use cpu hardware stack like makeGosubWithArgsViaCpuStack() in the statement reorderer - // we can't reuse tryReplaceCallWithGosub() from the StatementReorderer because we can't rewrite an expression node into a gosub *statement*... + require(callee.isAsmSubroutine) if(callee.parameters.isEmpty()) return - // load all arguments reversed onto the stack: first arg goes last (is on top). - for (arg in call.args.reversed()) - asmgen.translateExpression(arg) - - var argForCarry: IndexedValue>? = null - var argForXregister: IndexedValue>? = null - var argForAregister: IndexedValue>? = null - - asmgen.out(" inx") // align estack pointer - - for(argi in call.args.zip(callee.asmParameterRegisters).withIndex()) { - val plusIdxStr = if(argi.index==0) "" else "+${argi.index}" - when { - argi.value.second.statusflag == Statusflag.Pc -> { - require(argForCarry == null) - argForCarry = argi - } - argi.value.second.statusflag != null -> throw AssemblyError("can only use Carry as status flag parameter") - argi.value.second.registerOrPair in arrayOf(RegisterOrPair.X, RegisterOrPair.AX, RegisterOrPair.XY) -> { - require(argForXregister==null) - argForXregister = argi - } - argi.value.second.registerOrPair in arrayOf(RegisterOrPair.A, RegisterOrPair.AY) -> { - require(argForAregister == null) - argForAregister = argi - } - argi.value.second.registerOrPair == RegisterOrPair.Y -> { - asmgen.out(" ldy P8ESTACK_LO$plusIdxStr,x") - } - argi.value.second.registerOrPair in Cx16VirtualRegisters -> { - // immediately output code to load the virtual register, to avoid clobbering the A register later - when (callee.parameters[argi.index].type) { - in ByteDatatypes -> { - // only load the lsb of the virtual register - asmgen.out( - """ - lda P8ESTACK_LO$plusIdxStr,x - sta cx16.${argi.value.second.registerOrPair.toString().lowercase()} - """) - if (asmgen.isTargetCpu(CpuType.CPU65c02)) - asmgen.out( - " stz cx16.${ - argi.value.second.registerOrPair.toString().lowercase() - }+1") - else - asmgen.out( - " lda #0 | sta cx16.${ - argi.value.second.registerOrPair.toString().lowercase() - }+1") - } - in WordDatatypes, in IterableDatatypes -> - asmgen.out( - """ - lda P8ESTACK_LO$plusIdxStr,x - sta cx16.${argi.value.second.registerOrPair.toString().lowercase()} - lda P8ESTACK_HI$plusIdxStr,x - sta cx16.${ - argi.value.second.registerOrPair.toString().lowercase() - }+1 - """) - else -> throw AssemblyError("weird dt") - } - } - else -> throw AssemblyError("weird argument") - } + // use the cpu hardware stack as intermediate storage for the arguments. + val argOrder = asmgen.options.compTarget.asmsubArgsEvalOrder(callee) + argOrder.reversed().forEach { + asmgen.pushCpuStack(callee.parameters[it].type, call.args[it]) } - - if(argForCarry!=null) { - val plusIdxStr = if(argForCarry.index==0) "" else "+${argForCarry.index}" - asmgen.out(""" - clc - lda P8ESTACK_LO$plusIdxStr,x - beq + - sec -+ php""") // push the status flags + argOrder.forEach { + val param = callee.parameters[it] + val targetVar = callee.searchAsmParameter(param.name)!! + asmgen.popCpuStack(param.type, targetVar, (call as Node).definingSubroutine) } - - if(argForAregister!=null) { - val plusIdxStr = if(argForAregister.index==0) "" else "+${argForAregister.index}" - when(argForAregister.value.second.registerOrPair) { - RegisterOrPair.A -> asmgen.out(" lda P8ESTACK_LO$plusIdxStr,x") - RegisterOrPair.AY -> asmgen.out(" lda P8ESTACK_LO$plusIdxStr,x | ldy P8ESTACK_HI$plusIdxStr,x") - else -> throw AssemblyError("weird arg") - } - } - - if(argForXregister!=null) { - val plusIdxStr = if(argForXregister.index==0) "" else "+${argForXregister.index}" - - if(argForAregister!=null) - asmgen.out(" pha") - when(argForXregister.value.second.registerOrPair) { - RegisterOrPair.X -> asmgen.out(" lda P8ESTACK_LO$plusIdxStr,x | tax") - RegisterOrPair.AX -> asmgen.out(" ldy P8ESTACK_LO$plusIdxStr,x | lda P8ESTACK_HI$plusIdxStr,x | tax | tya") - RegisterOrPair.XY -> asmgen.out(" ldy P8ESTACK_HI$plusIdxStr,x | lda P8ESTACK_LO$plusIdxStr,x | tax") - else -> throw AssemblyError("weird arg") - } - if(argForAregister!=null) - asmgen.out(" pla") - } else { - repeat(callee.parameters.size - 1) { asmgen.out(" inx") } // unwind stack - } - - if(argForCarry!=null) - asmgen.out(" plp") // set the carry flag back to correct value } private fun argumentViaVariable(sub: Subroutine, parameter: SubroutineParameter, value: Expression) { diff --git a/codeOptimizers/src/prog8/optimizer/StatementOptimizer.kt b/codeOptimizers/src/prog8/optimizer/StatementOptimizer.kt index d7ac66145..2856caba2 100644 --- a/codeOptimizers/src/prog8/optimizer/StatementOptimizer.kt +++ b/codeOptimizers/src/prog8/optimizer/StatementOptimizer.kt @@ -127,10 +127,9 @@ class StatementOptimizer(private val program: Program, // see if we can optimize any complex argument expressions to be just a simple variable // TODO for now, only works for single-argument functions because we use just 1 temp var: R9 - // TODO is this still useful at all, when functioncallstatement gets replaced by GoSub? if(functionCallStatement.target.nameInSource !in listOf(listOf("pop"), listOf("popw")) && functionCallStatement.args.size==1) { val arg = functionCallStatement.args[0] - if(!arg.isSimple && arg !is TypecastExpression && arg !is IFunctionCall) { + if(!arg.isSimple && arg !is IFunctionCall) { val name = getTempRegisterName(arg.inferType(program)) val tempvar = IdentifierReference(name, functionCallStatement.position) val assignTempvar = Assignment(AssignTarget(tempvar.copy(), null, null, functionCallStatement.position), arg, AssignmentOrigin.OPTIMIZER, functionCallStatement.position) diff --git a/compiler/src/prog8/compiler/astprocessing/StatementReorderer.kt b/compiler/src/prog8/compiler/astprocessing/StatementReorderer.kt index 9ead6358c..acc7ac861 100644 --- a/compiler/src/prog8/compiler/astprocessing/StatementReorderer.kt +++ b/compiler/src/prog8/compiler/astprocessing/StatementReorderer.kt @@ -503,6 +503,30 @@ private fun makeGosubWithArgsViaCpuStack(call: IFunctionCall, callee: Subroutine, compTarget: ICompilationTarget): Iterable { + fun popCall(targetName: List, dt: DataType, position: Position): FunctionCallStatement { + return FunctionCallStatement( + IdentifierReference(listOf(if(dt in ByteDatatypes) "pop" else "popw"), position), + mutableListOf(IdentifierReference(targetName, position)), + true, position + ) + } + + fun pushCall(value: Expression, dt: DataType, position: Position): FunctionCallStatement { + val pushvalue = when(dt) { + DataType.UBYTE, DataType.UWORD -> value + in PassByReferenceDatatypes -> value + DataType.BYTE -> TypecastExpression(value, DataType.UBYTE, true, position) + DataType.WORD -> TypecastExpression(value, DataType.UWORD, true, position) + else -> throw FatalAstException("invalid dt $dt $value") + } + + return FunctionCallStatement( + IdentifierReference(listOf(if(dt in ByteDatatypes) "push" else "pushw"), position), + mutableListOf(pushvalue), + true, position + ) + } + val argOrder = compTarget.asmsubArgsEvalOrder(callee) val scope = AnonymousScope(mutableListOf(), position) if(callee.shouldSaveX()) { @@ -525,27 +549,3 @@ private fun makeGosubWithArgsViaCpuStack(call: IFunctionCall, } return listOf(IAstModification.ReplaceNode(call as Node, scope, parent)) } - -private fun popCall(targetName: List, dt: DataType, position: Position): FunctionCallStatement { - return FunctionCallStatement( - IdentifierReference(listOf(if(dt in ByteDatatypes) "pop" else "popw"), position), - mutableListOf(IdentifierReference(targetName, position)), - true, position - ) -} - -private fun pushCall(value: Expression, dt: DataType, position: Position): FunctionCallStatement { - val pushvalue = when(dt) { - DataType.UBYTE, DataType.UWORD -> value - in PassByReferenceDatatypes -> value - DataType.BYTE -> TypecastExpression(value, DataType.UBYTE, true, position) - DataType.WORD -> TypecastExpression(value, DataType.UWORD, true, position) - else -> throw FatalAstException("invalid dt $dt $value") - } - - return FunctionCallStatement( - IdentifierReference(listOf(if(dt in ByteDatatypes) "push" else "pushw"), position), - mutableListOf(pushvalue), - true, position - ) -} diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 157dd397f..e5d4c4d7a 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -3,14 +3,8 @@ TODO For next release ^^^^^^^^^^^^^^^^ -- attempt to rework registerArgsViaStackEvaluation() to use tempvars or cpu hardware stack instead of evalstack based evaluation -- ... UNLESS the expression (functioncall) node is the topmost node of the expression tree ??? - -- check if the optimization step for single arg func call statements to use R9 is still used/useful in after(functionCallStatement) in StatementOptimizer - -- all function call asmgen code should use the same routine to pass arguments... -- Also calls to builtin functions are still a FunctionCall node -> make new Node type for these if they're a Statement? - So that we at least get rid of the FunctionCallStatement altogether in the codegen. +- Calls to builtin functions are still a FunctionCall node -> make new Node type for these if they're a Statement? + So that we at least get rid of the FunctionCallStatement altogether in the codegen.? - see if we can get rid of storing the origAstTarget in AsmAssignTarget diff --git a/examples/test.p8 b/examples/test.p8 index df08151de..3882bb5e1 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -1,14 +1,28 @@ +%import textio main { sub start() { - ubyte xx = 10 - ubyte yy = 10 + ubyte xx = 200 + ubyte yy = 100 simple(xx+yy) void routine(xx+yy, yy+99, 99, true) uword @shared zz = mkword(xx+yy,yy+99) - zz = routine(xx+yy, yy+99, 99, true) + zz = routine(1000+xx+yy, yy+99, 55, true) + + txt.print("1300 199 55 1 ?:\n") + txt.print_uw(r_arg) + txt.spc() + txt.print_ub(r_arg2) + txt.spc() + txt.print_ub(r_arg3) + txt.spc() + txt.print_ub(r_arg4) + txt.spc() + memory.mem() + repeat { + } } uword @shared r_arg @@ -25,6 +39,7 @@ main { asmsub routine(uword arg @AY, ubyte arg2 @X, ubyte arg3 @R0, ubyte arg4 @Pc) -> ubyte @A { %asm {{ pha + lda #0 adc #0 sta r_arg4 pla