mirror of
https://github.com/irmen/prog8.git
synced 2025-09-26 01:16:46 +00:00
fix some struct type and symbol lookup errors
This commit is contained in:
@@ -128,7 +128,11 @@ class DataType private constructor(val base: BaseDataType, val sub: BaseDataType
|
||||
BaseDataType.UNDEFINED to DataType(BaseDataType.UNDEFINED, null, null)
|
||||
)
|
||||
|
||||
fun forDt(dt: BaseDataType) = simpletypes.getValue(dt)
|
||||
fun forDt(dt: BaseDataType): DataType {
|
||||
if(dt.isStructInstance)
|
||||
TODO("cannot use struct instance as a data type (yet) - use a pointer instead")
|
||||
return simpletypes.getValue(dt)
|
||||
}
|
||||
|
||||
fun arrayFor(elementDt: BaseDataType, splitwordarray: Boolean=true): DataType {
|
||||
require(!elementDt.isPointer) { "use other array constructor for arrays of pointers" }
|
||||
@@ -224,7 +228,7 @@ class DataType private constructor(val base: BaseDataType, val sub: BaseDataType
|
||||
if(sub!=null) "${sub.name.lowercase()}[]" else if (subType!=null) "${subType!!.scopedNameString}[]" else "${subTypeFromAntlr}[]"
|
||||
}
|
||||
BaseDataType.STRUCT_INSTANCE -> {
|
||||
if(sub!=null) sub.name.lowercase() else if (subType!=null) subType!!.scopedNameString else "$subTypeFromAntlr"
|
||||
sub?.name?.lowercase() ?: if (subType!=null) subType!!.scopedNameString else "$subTypeFromAntlr"
|
||||
}
|
||||
else -> base.name.lowercase()
|
||||
}
|
||||
|
@@ -989,6 +989,9 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
else
|
||||
ExpressionCodeResult(result, returnRegSpec!!.dt, finalReturnRegister, -1)
|
||||
}
|
||||
is StStruct -> {
|
||||
throw AssemblyError("stray struct constructor should have been removed (normally it can only occur as initialization expression for a pointer variable)")
|
||||
}
|
||||
else -> {
|
||||
if(callTarget.type == StNodeType.LABEL) {
|
||||
require(fcall.void)
|
||||
@@ -998,7 +1001,7 @@ internal class ExpressionGen(private val codeGen: IRCodeGen) {
|
||||
return ExpressionCodeResult(result, IRDataType.BYTE, -1, -1)
|
||||
}
|
||||
else {
|
||||
throw AssemblyError("invalid node type")
|
||||
throw AssemblyError("invalid node type ${callTarget.type} at ${callTarget.astNode?.position}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1889,7 +1889,7 @@ class IRCodeGen(
|
||||
}
|
||||
}
|
||||
}
|
||||
is PtStructDecl -> { /* do nothing */ }
|
||||
is PtStructDecl -> { /* do nothing, should be found in the symbol table */ }
|
||||
else -> TODO("weird block child node $child")
|
||||
}
|
||||
}
|
||||
|
@@ -27,6 +27,9 @@ class IRUnusedCodeRemover(
|
||||
// we could clean up the SymbolTable as well, but ONLY if these symbols aren't referenced somewhere still in an instruction or variable initializer value
|
||||
val prefix = "$blockLabel."
|
||||
val blockVars = irprog.st.allVariables().filter { it.name.startsWith(prefix) }
|
||||
|
||||
// check if there are symbols referenced elsewhere that we should not prune (even though the rest of the block is empty)
|
||||
|
||||
blockVars.forEach { stVar ->
|
||||
irprog.allSubs().flatMap { it.chunks }.forEach { chunk ->
|
||||
chunk.instructions.forEach { ins ->
|
||||
@@ -47,6 +50,19 @@ class IRUnusedCodeRemover(
|
||||
}
|
||||
}
|
||||
|
||||
val blockStructs = irprog.st.allStructDefs().filter { it.name.startsWith(prefix) }
|
||||
blockStructs.forEach { struct ->
|
||||
irprog.st.allStructInstances().forEach { instance ->
|
||||
if(instance.structName == struct.name)
|
||||
return // a struct instance is declared using this struct type
|
||||
}
|
||||
irprog.st.allVariables().forEach { variable ->
|
||||
if(variable.dt.isPointer || variable.dt.isStructInstance)
|
||||
if(struct.name == variable.dt.subType!!.scopedNameString)
|
||||
return // a variable exists with the struct as (pointer) type
|
||||
}
|
||||
}
|
||||
|
||||
irprog.st.removeTree(blockLabel)
|
||||
}
|
||||
|
||||
|
@@ -16,8 +16,8 @@ fun convertStToIRSt(sourceSt: SymbolTable?): IRSymbolTable {
|
||||
StNodeType.MEMORYSLAB -> st.add(convert(it.value as StMemorySlab))
|
||||
StNodeType.STRUCTINSTANCE -> {
|
||||
val instance = it.value as StStructInstance
|
||||
val struct = st.lookup(instance.structName) as IRStStructDef
|
||||
st.add(convert(instance, struct))
|
||||
val struct = sourceSt.lookup(instance.structName) as StStruct
|
||||
st.add(convert(instance, struct.fields))
|
||||
}
|
||||
StNodeType.STRUCT -> st.add(convert(it.value as StStruct))
|
||||
else -> { }
|
||||
@@ -137,8 +137,8 @@ private fun convert(variable: StMemorySlab): IRStMemorySlab {
|
||||
}
|
||||
|
||||
|
||||
private fun convert(instance: StStructInstance, struct: IRStStructDef): IRStStructInstance {
|
||||
val values = struct.fields.zip(instance.initialValues).map { (field, value) ->
|
||||
private fun convert(instance: StStructInstance, fields: List<Pair<DataType, String>>): IRStStructInstance {
|
||||
val values = fields.zip(instance.initialValues).map { (field, value) ->
|
||||
val elt = convertArrayElt(value)
|
||||
IRStructInitValue(field.first.base, elt)
|
||||
}
|
||||
|
@@ -181,14 +181,20 @@ class UnusedCodeRemover(private val program: Program,
|
||||
val declIndex = (parent as IStatementContainer).statements.indexOf(decl)
|
||||
val singleUseIndex = (parent as IStatementContainer).statements.indexOf(singleUse.parent)
|
||||
if(declIndex==singleUseIndex-1) {
|
||||
if("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("replaced unused variable '${decl.name}' with void call, maybe this can be removed altogether", decl.position)
|
||||
val fcall = assignment.value as IFunctionCall
|
||||
val voidCall = FunctionCallStatement(fcall.target, fcall.args, true, fcall.position)
|
||||
return listOf(
|
||||
IAstModification.ReplaceNode(decl, voidCall, parent),
|
||||
IAstModification.Remove(assignment, assignment.parent as IStatementContainer)
|
||||
)
|
||||
val callStruct = (assignment.value as IFunctionCall).target.targetStructDecl()
|
||||
if(callStruct!=null) {
|
||||
// don't turn a struct instance allocation call to a void call, instead, remove everything
|
||||
return listOf(IAstModification.Remove(assignment, assignment.parent as IStatementContainer))
|
||||
} else {
|
||||
if("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("replaced unused variable '${decl.name}' with void call, maybe this can be removed altogether", decl.position)
|
||||
val fcall = assignment.value as IFunctionCall
|
||||
val voidCall = FunctionCallStatement(fcall.target, fcall.args, true, fcall.position)
|
||||
return listOf(
|
||||
IAstModification.ReplaceNode(decl, voidCall, parent),
|
||||
IAstModification.Remove(assignment, assignment.parent as IStatementContainer)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
@@ -569,11 +569,17 @@ internal class AstChecker(private val program: Program,
|
||||
if (p.type.isPointer) {
|
||||
if (p.type.subType == null && p.type.subTypeFromAntlr!=null) errors.err("cannot find struct type ${p.type.subTypeFromAntlr?.joinToString(".")}", p.position)
|
||||
}
|
||||
|
||||
if(p.type.isStructInstance)
|
||||
errors.err("structs can only be passed via a pointer", p.position)
|
||||
}
|
||||
|
||||
for((index, r) in subroutine.returntypes.withIndex()) {
|
||||
if(r.isPointer && r.subType==null && r.subTypeFromAntlr!=null)
|
||||
err("return type #${index+1}: cannot find struct type ${r.subTypeFromAntlr?.joinToString(".")}")
|
||||
|
||||
if(r.isStructInstance)
|
||||
err("structs can only be returned via a pointer")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1603,6 +1609,10 @@ internal class AstChecker(private val program: Program,
|
||||
errors.err("function doesn't return a value", functionCallExpr.position)
|
||||
}
|
||||
}
|
||||
else if(targetStatement is StructDecl) {
|
||||
if(functionCallExpr.parent is IStatementContainer)
|
||||
errors.err("static struct instance allocation can only occur as an initializer for a pointer variable", functionCallExpr.position)
|
||||
}
|
||||
|
||||
if(builtinFunctionName in listOf("peek", "peekw")) {
|
||||
val pointervar = functionCallExpr.args[0] as? IdentifierReference
|
||||
@@ -1632,6 +1642,10 @@ internal class AstChecker(private val program: Program,
|
||||
checkFunctionCall(targetStatement, functionCallStatement.args, functionCallStatement.position)
|
||||
checkUnusedReturnValues(functionCallStatement, targetStatement, errors)
|
||||
|
||||
if(targetStatement is StructDecl) {
|
||||
errors.err("static struct instance allocation can only occur as an initializer for a pointer variable", functionCallStatement.position)
|
||||
}
|
||||
|
||||
if(functionCallStatement.void) {
|
||||
when(targetStatement) {
|
||||
is BuiltinFunctionPlaceholder -> {
|
||||
@@ -1808,11 +1822,14 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO rest
|
||||
|
||||
// TODO rest?
|
||||
}
|
||||
|
||||
args.forEach{
|
||||
checkLongType(it)
|
||||
if(it.inferType(program).isStructInstance)
|
||||
errors.err("structs can only be passed via a pointer", it.position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2019,9 +2036,9 @@ internal class AstChecker(private val program: Program,
|
||||
errors.err("unable to determine type of dereferenced pointer expression", deref.position)
|
||||
}
|
||||
|
||||
override fun visit(deref: PtrIndexedDereference) {
|
||||
override fun visit(idxderef: PtrIndexedDereference) {
|
||||
if(compilerOptions.compTarget.name != VMTarget.NAME)
|
||||
TODO("typed pointers are not yet supported in the 6502 compilation targets (only virtual) ${deref.position}")
|
||||
TODO("typed pointers are not yet supported in the 6502 compilation targets (only virtual) ${idxderef.position}")
|
||||
|
||||
// TODO ast checks for this one?
|
||||
}
|
||||
|
@@ -398,11 +398,11 @@ class SimplifiedAstMaker(private val program: Program, private val errors: IErro
|
||||
|
||||
val (target, _) = srcCall.target.targetNameAndType(program)
|
||||
val iType = srcCall.inferType(program)
|
||||
val subtypeStruct = iType.getOrUndef().subType
|
||||
val call =
|
||||
if(iType.isStructInstance) {
|
||||
// a call to a struct yields a struct instance and means: allocate a statically initialized struct instance of that type
|
||||
val struct = iType.getOrUndef().subType!!
|
||||
val pointertype = DataType.pointerToType(struct)
|
||||
if(subtypeStruct!=null) {
|
||||
// a call to a struct yields a pointer to a struct instance and means: allocate a statically initialized struct instance of that type
|
||||
val pointertype = DataType.pointerToType(subtypeStruct)
|
||||
PtBuiltinFunctionCall("structalloc", false, true, pointertype, srcCall.position)
|
||||
} else {
|
||||
// regular function call
|
||||
|
@@ -292,7 +292,7 @@ class TypecastsAdder(val program: Program, val options: CompilationOptions, val
|
||||
val possibleTargetDt = it.first.possibleDatatypes.first()
|
||||
val targetDt = if(possibleTargetDt.isPointer) BaseDataType.UWORD else possibleTargetDt // use UWORD instead of a pointer type (using words for pointers is allowed without further casting)
|
||||
val argIdt = it.second.inferType(program)
|
||||
if (argIdt.isKnown) {
|
||||
if (argIdt.isKnown && !targetDt.isStructInstance) {
|
||||
val argDt = argIdt.getOrUndef()
|
||||
if (argDt.base !in it.first.possibleDatatypes) {
|
||||
val identifier = it.second as? IdentifierReference
|
||||
|
@@ -447,6 +447,14 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
|
||||
return listOf(IAstModification.ReplaceNode(functionCallExpr, cast, parent))
|
||||
}
|
||||
}
|
||||
|
||||
if(parent is IStatementContainer) {
|
||||
val targetStruct = functionCallExpr.target.targetStructDecl()
|
||||
if (targetStruct != null) {
|
||||
// static struct instance allocation can only occur as an initializer for a pointer variable
|
||||
return listOf(IAstModification.Remove(functionCallExpr, parent as IStatementContainer))
|
||||
}
|
||||
}
|
||||
return noModifications
|
||||
}
|
||||
|
||||
|
@@ -1,11 +1,18 @@
|
||||
package prog8tests.compiler
|
||||
|
||||
import io.kotest.assertions.withClue
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.shouldNotBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import io.kotest.matchers.types.instanceOf
|
||||
import prog8.code.ast.PtReturn
|
||||
import prog8.code.ast.PtSubSignature
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.vm.VmRunner
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import prog8tests.helpers.compileText
|
||||
import kotlin.io.path.readText
|
||||
|
||||
@@ -38,4 +45,169 @@ thing {
|
||||
VmRunner().runProgram(virtfile.readText(), true)
|
||||
}
|
||||
|
||||
test("passing struct instances to subroutines and returning a struct instance is not allowed") {
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
}
|
||||
|
||||
struct Node {
|
||||
bool flag
|
||||
}
|
||||
|
||||
sub faulty(Node arg) -> Node {
|
||||
return cx16.r0
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors=errors)
|
||||
val err = errors.errors
|
||||
err.size shouldBe 3
|
||||
err[0] shouldContain "uword doesn't match"
|
||||
err[1] shouldContain "structs can only be passed via a pointer"
|
||||
err[2] shouldContain "structs can only be returned via a pointer"
|
||||
}
|
||||
|
||||
test("pointers in subroutine return values") {
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
^^thing.Node @shared ptr = thing.new()
|
||||
}
|
||||
}
|
||||
|
||||
thing {
|
||||
struct Node {
|
||||
bool flag
|
||||
^^Node next
|
||||
}
|
||||
|
||||
sub new() -> ^^Node {
|
||||
cx16.r0++
|
||||
^^Node pointer = 2000
|
||||
return pointer
|
||||
}
|
||||
}"""
|
||||
|
||||
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
|
||||
// TODO compileText(C64Target(), false, src, outputDir) shouldNotBe null
|
||||
}
|
||||
|
||||
|
||||
test("creating instances") {
|
||||
val src="""
|
||||
main {
|
||||
struct MyNode {
|
||||
bool flag
|
||||
^^MyNode next
|
||||
}
|
||||
|
||||
sub start() {
|
||||
^^MyNode @shared m1 = MyNode()
|
||||
^^MyNode @shared m2 = MyNode(true, 0)
|
||||
|
||||
^^thing.Node @shared n1 = thing.Node()
|
||||
^^thing.Node @shared n2 = thing.Node(true, 0)
|
||||
}
|
||||
}
|
||||
|
||||
thing {
|
||||
struct Node {
|
||||
bool flag
|
||||
^^Node next
|
||||
}
|
||||
}"""
|
||||
|
||||
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
|
||||
compileText(VMTarget(), true, src, outputDir) shouldNotBe null
|
||||
// TODO compileText(C64Target(), false, src, outputDir) shouldNotBe null
|
||||
// TODO compileText(C64Target(), true, src, outputDir) shouldNotBe null
|
||||
}
|
||||
|
||||
|
||||
test("creating instances with optimization should all be removed") {
|
||||
val src="""
|
||||
main {
|
||||
struct MyNode {
|
||||
bool flag
|
||||
^^MyNode next
|
||||
}
|
||||
|
||||
sub start() {
|
||||
^^MyNode m1 = MyNode()
|
||||
^^MyNode m2 = MyNode(true, 0)
|
||||
|
||||
^^thing.Node n1 = thing.Node()
|
||||
^^thing.Node n2 = thing.Node(true, 0)
|
||||
}
|
||||
}
|
||||
|
||||
thing {
|
||||
struct Node {
|
||||
bool flag
|
||||
^^Node next
|
||||
}
|
||||
}"""
|
||||
|
||||
var result = compileText(VMTarget(), true, src, outputDir)!!
|
||||
withClue("all variables should have been optimized away") {
|
||||
val start = result.codegenAst!!.entrypoint()!!
|
||||
start.children.size shouldBe 2
|
||||
start.children[0] shouldBe instanceOf<PtSubSignature>()
|
||||
start.children[1] shouldBe instanceOf<PtReturn>()
|
||||
}
|
||||
// TODO compileText(C64Target(), true, src, outputDir) shouldNotBe null
|
||||
}
|
||||
|
||||
|
||||
test("creating instances should have correct number of args") {
|
||||
val src="""
|
||||
main {
|
||||
struct Node {
|
||||
bool flag
|
||||
ubyte value
|
||||
^^Node next
|
||||
}
|
||||
|
||||
sub start() {
|
||||
^^Node ptr = Node(true) ; error
|
||||
}
|
||||
}"""
|
||||
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors=errors)
|
||||
val err = errors.errors
|
||||
err.size shouldBe 1
|
||||
err[0] shouldContain("expected 3 or 0, got 1")
|
||||
}
|
||||
|
||||
|
||||
test("pointer uword compatibility") {
|
||||
val src="""
|
||||
main {
|
||||
struct MyNode {
|
||||
bool flag
|
||||
^^MyNode next
|
||||
}
|
||||
|
||||
sub start() {
|
||||
cx16.r0 = MyNode()
|
||||
|
||||
^^MyNode @shared ptr1 = cx16.r0
|
||||
|
||||
ptr1 = 2000
|
||||
ptr1 = 20
|
||||
ptr1 = 20.2222
|
||||
}
|
||||
}"""
|
||||
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors=errors)
|
||||
val err = errors.errors
|
||||
err.size shouldBe 1
|
||||
err[0] shouldContain("15:16: can only assign uword or correct pointer type to a pointer")
|
||||
}
|
||||
|
||||
})
|
@@ -73,7 +73,7 @@ class TestSubroutines: FunSpec({
|
||||
func.statements.isEmpty() shouldBe true
|
||||
}
|
||||
|
||||
test("cannot call a subroutine via pointer") {
|
||||
test("cannot call a subroutine via a pointer") {
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
|
@@ -1458,7 +1458,8 @@ class FunctionCallExpression(override var target: IdentifierReference,
|
||||
return InferredTypes.unknown() // has multiple return types... so not a single resulting datatype possible
|
||||
}
|
||||
is StructDecl -> {
|
||||
return InferredTypes.knownFor(DataType.structInstance(stmt))
|
||||
// calling a struct is syntax for allocating a static instance, and returns a pointer to that (not the instance itself)
|
||||
return InferredTypes.knownFor(DataType.pointerToType(stmt))
|
||||
}
|
||||
else -> return InferredTypes.unknown()
|
||||
}
|
||||
|
@@ -109,7 +109,10 @@ object InferredTypes {
|
||||
}
|
||||
type.isPointerArray -> InferredType.known(DataType.arrayOfPointersTo(type.sub, type.subType))
|
||||
type.isStructInstance -> {
|
||||
InferredType.known(DataType.structInstance(type.subType!!))
|
||||
if(type.subType!=null)
|
||||
InferredType.known(DataType.structInstance(type.subType!!))
|
||||
else
|
||||
InferredType.known(DataType.structInstanceFromAntlr(type.subTypeFromAntlr!!))
|
||||
}
|
||||
else -> throw IllegalArgumentException("invalid type $type")
|
||||
}
|
||||
|
@@ -1,47 +1,16 @@
|
||||
%import textio
|
||||
|
||||
main {
|
||||
struct MyNode {
|
||||
bool flag
|
||||
^^MyNode next
|
||||
}
|
||||
|
||||
sub start() {
|
||||
^^uword ptr = 2000
|
||||
cx16.r0 = MyNode()
|
||||
|
||||
ptr^^ <<= 2
|
||||
ptr^^ >>= 3
|
||||
^^MyNode @shared ptr1 = cx16.r0
|
||||
|
||||
pokew(2000, 1111)
|
||||
|
||||
cx16.r0 ^= $ffff
|
||||
|
||||
txt.print_uw(ptr^^)
|
||||
txt.nl()
|
||||
ptr^^ += 5
|
||||
txt.print_uw(ptr^^)
|
||||
txt.nl()
|
||||
ptr^^ -= 9
|
||||
txt.print_uw(ptr^^)
|
||||
txt.nl()
|
||||
ptr^^ *= 3
|
||||
txt.print_uw(ptr^^)
|
||||
txt.nl()
|
||||
ptr^^ /= 3
|
||||
txt.print_uw(ptr^^)
|
||||
txt.nl()
|
||||
ptr^^ |= $7f0f
|
||||
txt.print_uwhex(ptr^^,true)
|
||||
txt.nl()
|
||||
ptr^^ &= $f0f0
|
||||
txt.print_uwhex(ptr^^,true)
|
||||
txt.nl()
|
||||
ptr^^ ^= $ffff
|
||||
txt.print_uwhex(ptr^^,true)
|
||||
txt.nl()
|
||||
|
||||
; const uword buffer = $2000
|
||||
; uword @shared addr = &buffer[2]
|
||||
;
|
||||
; const ubyte width = 100
|
||||
; ubyte @shared i
|
||||
; ubyte @shared j
|
||||
; uword @shared addr2 = &buffer[i * width + j]
|
||||
; txt.print_uw(addr)
|
||||
ptr1 = 2000
|
||||
ptr1 = 20
|
||||
ptr1 = 20.2222
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,10 @@
|
||||
package prog8.intermediate
|
||||
|
||||
import prog8.code.INTERNED_STRINGS_MODULENAME
|
||||
import prog8.code.core.BaseDataType
|
||||
import prog8.code.core.DataType
|
||||
import prog8.code.core.Encoding
|
||||
import prog8.code.core.ZeropageWish
|
||||
import prog8.code.core.BaseDataType
|
||||
|
||||
|
||||
// In the Intermediate Representation, all nesting has been removed.
|
||||
@@ -30,6 +30,9 @@ class IRSymbolTable {
|
||||
fun allStructInstances(): Sequence<IRStStructInstance> =
|
||||
table.asSequence().map { it.value }.filterIsInstance<IRStStructInstance>()
|
||||
|
||||
fun allStructDefs(): Sequence<IRStStructDef> =
|
||||
table.asSequence().map { it.value }.filterIsInstance<IRStStructDef>()
|
||||
|
||||
fun lookup(name: String): IRStNode? = table[name]
|
||||
|
||||
fun add(node: IRStNode) {
|
||||
|
Reference in New Issue
Block a user