diff --git a/README.md b/README.md index 302b41111..9e199ff2a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ which aims to provide many conveniences over raw assembly code (even when using - constant folding in expressions (compile-time evaluation) - conditional branches - when statement to provide a 'jump table' alternative to if/elseif chains +- structs to group together sets of variables and manipulate them at once - automatic type conversions - floating point operations (uses the C64 Basic ROM routines for this) - abstracting away low level aspects such as ZeroPage handling, program startup, explicit memory addresses diff --git a/compiler/src/prog8/ast/AstToplevel.kt b/compiler/src/prog8/ast/AstToplevel.kt index b25c8dbc1..5c97f4a14 100644 --- a/compiler/src/prog8/ast/AstToplevel.kt +++ b/compiler/src/prog8/ast/AstToplevel.kt @@ -71,6 +71,21 @@ class GlobalNamespace(val modules: List): Node, INameScope { return builtinPlaceholder } + if(scopedName.size>1) { + // a scoped name can a) refer to a member of a struct, or b) refer to a name in another module. + // try the struct first. + val thing = lookup(scopedName.dropLast(1), localContext) as? VarDecl + val struct = thing?.struct + if (struct != null) { + if(struct.statements.any { (it as VarDecl).name == scopedName.last()}) { + // return ref to the mangled name variable + val mangled = mangledStructMemberName(thing.name, scopedName.last()) + val mangledVar = thing.definingScope().getLabelOrVariable(mangled) + return mangledVar + } + } + } + val stmt = localContext.definingModule().lookup(scopedName, localContext) return when (stmt) { is Label, is VarDecl, is Block, is Subroutine -> stmt diff --git a/compiler/src/prog8/ast/Interfaces.kt b/compiler/src/prog8/ast/Interfaces.kt index 35641296d..aa77a8422 100644 --- a/compiler/src/prog8/ast/Interfaces.kt +++ b/compiler/src/prog8/ast/Interfaces.kt @@ -137,7 +137,7 @@ interface INameScope { // - the name of a symbol somewhere else starting from the root of the namespace. // check struct first - if(scopedName.size==2) { // TODO support for referencing structs in other scopes + if(scopedName.size==2) { // TODO support for referencing structs in other scopes . see GlobalNamespace? val mangledname = mangledStructMemberName(scopedName[0], scopedName[1]) val vardecl = localContext.definingScope().getLabelOrVariable(mangledname) if(vardecl!=null) diff --git a/compiler/src/prog8/ast/processing/AstChecker.kt b/compiler/src/prog8/ast/processing/AstChecker.kt index 0eb29c3f6..c104b6e6d 100644 --- a/compiler/src/prog8/ast/processing/AstChecker.kt +++ b/compiler/src/prog8/ast/processing/AstChecker.kt @@ -350,6 +350,17 @@ internal class AstChecker(private val program: Program, } } + val sourceIdent = assignment.value as? IdentifierReference + val targetIdent = assignment.target.identifier + if(sourceIdent!=null && targetIdent!=null) { + val sourceVar = sourceIdent.targetVarDecl(program.namespace) + val targetVar = targetIdent.targetVarDecl(program.namespace) + if(sourceVar?.struct!=null && targetVar?.struct!=null) { + if(sourceVar.struct!==targetVar.struct) + checkResult.add(ExpressionError("assignment of different struct types", assignment.position)) + } + } + var resultingAssignment = assignment resultingAssignment = processAssignmentTarget(resultingAssignment, assignment.target) return super.visit(resultingAssignment) @@ -1275,8 +1286,8 @@ internal class AstChecker(private val program: Program, else { if(decl.zeropage) checkResult.add(SyntaxError("struct can not contain zeropage members", decl.position)) - if(decl.datatype==DataType.STRUCT) - checkResult.add(SyntaxError("structs can not be nested", decl.position)) + if(decl.datatype !in NumericDatatypes) + checkResult.add(SyntaxError("structs can only contain numerical types", decl.position)) } } diff --git a/compiler/src/prog8/ast/processing/AstIdentifiersChecker.kt b/compiler/src/prog8/ast/processing/AstIdentifiersChecker.kt index ba9ce994c..98dcec05f 100644 --- a/compiler/src/prog8/ast/processing/AstIdentifiersChecker.kt +++ b/compiler/src/prog8/ast/processing/AstIdentifiersChecker.kt @@ -62,6 +62,9 @@ internal class AstIdentifiersChecker(private val namespace: INameScope) : IAstMo if(decl.structHasBeenFlattened) return decl // don't do this multiple times + if(decl.struct!!.statements.any { (it as VarDecl).datatype !in NumericDatatypes}) + return decl // a non-numeric member, not supported. proper error is given by AstChecker later + val decls: MutableList = decl.struct!!.statements.withIndex().map { val member = it.value as VarDecl val initvalue = if(decl.value!=null) (decl.value as LiteralValue).arrayvalue!![it.index] else null @@ -245,4 +248,14 @@ internal class AstIdentifiersChecker(private val namespace: INameScope) : IAstMo return super.visit(addressOf) } + override fun visit(structDecl: StructDecl): IStatement { + for(member in structDecl.statements){ + val decl = member as? VarDecl + if(decl!=null && decl.datatype !in NumericDatatypes) + checkResult.add(SyntaxError("structs can only contain numerical types", decl.position)) + } + + return super.visit(structDecl) + } + } diff --git a/compiler/src/prog8/ast/processing/StatementReorderer.kt b/compiler/src/prog8/ast/processing/StatementReorderer.kt index 2623a659f..309041fcc 100644 --- a/compiler/src/prog8/ast/processing/StatementReorderer.kt +++ b/compiler/src/prog8/ast/processing/StatementReorderer.kt @@ -2,14 +2,58 @@ package prog8.ast.processing import kotlin.comparisons.nullsLast import prog8.ast.* -import prog8.ast.base.DataType -import prog8.ast.base.FatalAstException +import prog8.ast.base.* import prog8.ast.base.initvarsSubName -import prog8.ast.base.printWarning import prog8.ast.expressions.* import prog8.ast.statements.* import prog8.functions.BuiltinFunctions + +fun flattenStructAssignment(structAssignment: Assignment, program: Program): List { + val identifier = structAssignment.target.identifier!! + val identifierName = identifier.nameInSource.single() + val targetVar = identifier.targetVarDecl(program.namespace)!! + val struct = targetVar.struct!! + val sourceVar = (structAssignment.value as IdentifierReference).targetVarDecl(program.namespace)!! + if(!sourceVar.isArray && sourceVar.struct==null) + throw FatalAstException("can only assign arrays or structs to structs") + if(sourceVar.isArray) { + val sourceArray = (sourceVar.value as LiteralValue).arrayvalue!! + return struct.statements.zip(sourceArray).map { member -> + val decl = member.first as VarDecl + val mangled = mangledStructMemberName(identifierName, decl.name) + val idref = IdentifierReference(listOf(mangled), structAssignment.position) + val assign = Assignment(AssignTarget(null, idref, null, null, structAssignment.position), + null, member.second, member.second.position) + assign.linkParents(structAssignment) + assign + } + } + else { + // struct memberwise copy + val sourceStruct = sourceVar.struct!! + if(sourceStruct!==targetVar.struct) { + // structs are not the same in assignment + return listOf() // error will be printed elsewhere + } + return struct.statements.zip(sourceStruct.statements).map { member -> + val targetDecl = member.first as VarDecl + val sourceDecl = member.second as VarDecl + if(targetDecl.name != sourceDecl.name) + throw FatalAstException("struct member mismatch") + val mangled = mangledStructMemberName(identifierName, targetDecl.name) + val idref = IdentifierReference(listOf(mangled), structAssignment.position) + val sourcemangled = mangledStructMemberName(sourceVar.name, sourceDecl.name) + val sourceIdref = IdentifierReference(listOf(sourcemangled), structAssignment.position) + val assign = Assignment(AssignTarget(null, idref, null, null, structAssignment.position), + null, sourceIdref, member.second.position) + assign.linkParents(structAssignment) + assign + } + } +} + + internal class StatementReorderer(private val program: Program): IAstModifyingVisitor { // Reorders the statements in a way the compiler needs. // - 'main' block must be the very first statement UNLESS it has an address set. @@ -174,12 +218,28 @@ internal class StatementReorderer(private val program: Program): IAstModifyingVi // see if a typecast is needed to convert the value's type into the proper target type val valuetype = assignment.value.inferType(program) val targettype = assignment.target.inferType(program, assignment) - if(targettype!=null && valuetype!=null && valuetype!=targettype) { - if(valuetype isAssignableTo targettype) { - assignment.value = TypecastExpression(assignment.value, targettype, true, assignment.value.position) - assignment.value.linkParents(assignment) + if(targettype!=null && valuetype!=null) { + if(valuetype!=targettype) { + if (valuetype isAssignableTo targettype) { + assignment.value = TypecastExpression(assignment.value, targettype, true, assignment.value.position) + assignment.value.linkParents(assignment) + } + // if they're not assignable, we'll get a proper error later from the AstChecker + } + } + + // struct assignments will be flattened + if(valuetype==DataType.STRUCT && targettype==DataType.STRUCT) { + val assignments = flattenStructAssignment(assignment, program) + if(assignments.isEmpty()) { + // something went wrong (probably incompatible struct types) + // we'll get an error later from the AstChecker + return assignment + } else { + val scope = AnonymousScope(assignments.toMutableList(), assignment.position) + scope.linkParents(assignment.parent) + return scope } - // if they're not assignable, we'll get a proper error later from the AstChecker } return super.visit(assignment) diff --git a/compiler/src/prog8/compiler/Compiler.kt b/compiler/src/prog8/compiler/Compiler.kt index 2fe0f37fb..a4fd311af 100644 --- a/compiler/src/prog8/compiler/Compiler.kt +++ b/compiler/src/prog8/compiler/Compiler.kt @@ -4,6 +4,7 @@ import prog8.ast.* import prog8.ast.base.* import prog8.ast.base.RegisterOrPair.* import prog8.ast.expressions.* +import prog8.ast.processing.flattenStructAssignment import prog8.ast.statements.* import prog8.compiler.intermediate.IntermediateProgram import prog8.compiler.intermediate.Opcode @@ -1464,26 +1465,6 @@ internal class Compiler(private val program: Program) { popValueIntoTarget(stmt.target, datatype) } - private fun flattenStructAssignment(structAssignment: Assignment, program: Program): List { - val identifier = structAssignment.target.identifier!! - val identifierName = identifier.nameInSource.single() - val targetVar = identifier.targetVarDecl(program.namespace)!! - val struct = targetVar.struct!! - val sourceVar = (structAssignment.value as IdentifierReference).targetVarDecl(program.namespace)!! - if(!sourceVar.isArray) - throw CompilerException("can only assign arrays to structs") - val sourceArray = (sourceVar.value as LiteralValue).arrayvalue!! - return struct.statements.zip(sourceArray).map { member -> - val decl = member.first as VarDecl - val mangled = mangledStructMemberName(identifierName, decl.name) - val idref = IdentifierReference(listOf(mangled), structAssignment.position) - val assign = Assignment(AssignTarget(null, idref, null, null, structAssignment.position), - null, member.second, member.second.position) - assign.linkParents(structAssignment) - assign - } - } - private fun pushHeapVarAddress(value: IExpression, removeLastOpcode: Boolean) { when (value) { is LiteralValue -> throw CompilerException("can only push address of string or array (value on the heap)") diff --git a/compiler/src/prog8/optimizer/CallGraph.kt b/compiler/src/prog8/optimizer/CallGraph.kt index ffba9fbbf..62f09e162 100644 --- a/compiler/src/prog8/optimizer/CallGraph.kt +++ b/compiler/src/prog8/optimizer/CallGraph.kt @@ -1,6 +1,7 @@ package prog8.optimizer import prog8.ast.* +import prog8.ast.base.DataType import prog8.ast.base.ParentSentinel import prog8.ast.base.VarDeclType import prog8.ast.base.initvarsSubName @@ -122,6 +123,10 @@ class CallGraph(private val program: Program): IAstVisitor { // make sure autogenerated vardecls are in the used symbols addNodeAndParentScopes(decl) } + + if(decl.datatype==DataType.STRUCT) + addNodeAndParentScopes(decl) + super.visit(decl) } @@ -196,10 +201,12 @@ class CallGraph(private val program: Program): IAstVisitor { if (matches2 != null) { val target= matches2.groups[2]?.value if (target != null && (target[0].isLetter() || target[0] == '_')) { - val node = program.namespace.lookup(listOf(target.substringBefore('.')), context) - if (node is Subroutine) { - subroutinesCalling[scope] = subroutinesCalling.getValue(scope).plus(node) - subroutinesCalledBy[node] = subroutinesCalledBy.getValue(node).plus(context) + if(target.contains('.')) { + val node = program.namespace.lookup(listOf(target.substringBefore('.')), context) + if (node is Subroutine) { + subroutinesCalling[scope] = subroutinesCalling.getValue(scope).plus(node) + subroutinesCalledBy[node] = subroutinesCalledBy.getValue(node).plus(context) + } } } } diff --git a/compiler/src/prog8/vm/astvm/VariablesCreator.kt b/compiler/src/prog8/vm/astvm/VariablesCreator.kt index 704c117cb..5c6dba7d3 100644 --- a/compiler/src/prog8/vm/astvm/VariablesCreator.kt +++ b/compiler/src/prog8/vm/astvm/VariablesCreator.kt @@ -40,8 +40,10 @@ class VariablesCreator(private val runtimeVariables: RuntimeVariables, private v when (decl.type) { // we can assume the value in the vardecl already has been converted into a constant LiteralValue here. VarDeclType.VAR -> { - val value = RuntimeValue.from(decl.value as LiteralValue, heap) - runtimeVariables.define(decl.definingScope(), decl.name, value) + if(decl.datatype!=DataType.STRUCT) { + val value = RuntimeValue.from(decl.value as LiteralValue, heap) + runtimeVariables.define(decl.definingScope(), decl.name, value) + } } VarDeclType.MEMORY -> { runtimeVariables.defineMemory(decl.definingScope(), decl.name, (decl.value as LiteralValue).asIntegerValue!!) diff --git a/docs/source/programming.rst b/docs/source/programming.rst index f39e080a5..5b2bce652 100644 --- a/docs/source/programming.rst +++ b/docs/source/programming.rst @@ -273,6 +273,38 @@ you have to use the ``str_s`` variants of the string type identifier. The same is true for arrays by the way. +Structs +^^^^^^^ + +A struct is a group of one or more other variables. +This allows you to reuse the definition and manipulate it as a whole. +Individual variables in the struct are accessed as you would expect, just +use a scoped name to refer to them: ``structvariable.membername``. + +Structs are a bit limited in Prog8: you can only use numerical variables +as member of a struct, so strings and arrays and other structs can not be part of a struct. +Also, it is not possible to use a struct itself inside an array. + +Structs are mainly syntactic sugar for repeated groups of vardecls +and assignments that belong together. + +To create a variable of a struct type you need to define the struct itself, +and then create a variable with it:: + + struct Color { + ubyte red + ubyte green + ubyte blue + } + + Color rgb = [255,122,0] + Color another ; the init value is optional, like arrays + + another = rgb ; assign all of the values of rgb to another + another.blue = 255 ; set a single member + + + Special types: const and memory-mapped ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ diff --git a/docs/source/syntaxreference.rst b/docs/source/syntaxreference.rst index 9a3df80c4..136c80a39 100644 --- a/docs/source/syntaxreference.rst +++ b/docs/source/syntaxreference.rst @@ -234,7 +234,7 @@ Various examples:: byte[5] values = 255 ; initialize with five 255 bytes word @zp zpword = 9999 ; prioritize this when selecting vars for zeropage storage - + Color rgb = [1,255,0] ; a struct variable Data types @@ -358,6 +358,22 @@ Syntax is familiar with brackets: ``arrayvar[x]`` :: string[4] ; the fifth character (=byte) in the string +Struct +^^^^^^ + +A *struct* has to be defined to specify what its member variables are. +There are one or more members:: + + struct { + + [ ...] + } + +You can only use numerical variables as member of a struct, so strings and arrays +and other structs can not be part of a struct. Vice versa, a struct can not occur in an array. + +After defining a struct you can use the name of the struct as a data type to declare variables with. + Operators --------- diff --git a/docs/source/todo.rst b/docs/source/todo.rst index 0f6d9040a..5edaa47e3 100644 --- a/docs/source/todo.rst +++ b/docs/source/todo.rst @@ -52,25 +52,6 @@ Allocate a fixed word in ZP that is the TOS so we can operate on TOS directly without having to to index into the stack? -structs? -^^^^^^^^ - -A user defined struct type would be nice to group a bunch -of values together (and use it multiple times). Something like:: - - struct Point { - ubyte color - word[] vec = [0,0,0] - } - - Point p1 - Point p2 - Point p3 - - p1.color = 3 - p1.vec[2] = 2 - - Misc ^^^^ diff --git a/examples/swirl.p8 b/examples/swirl.p8 index 7a7974560..0098268e1 100644 --- a/examples/swirl.p8 +++ b/examples/swirl.p8 @@ -5,20 +5,24 @@ const uword width = 40 const uword height = 25 - sub start() { - + struct Ball { uword anglex uword angley ubyte color + } + + sub start() { + + Ball ball while true { - ubyte x = msb(sin8u(msb(anglex)) as uword * width) - ubyte y = msb(cos8u(msb(angley)) as uword * height) - c64scr.setcc(x, y, 81, color) + ubyte x = msb(sin8u(msb(ball.anglex)) as uword * width) + ubyte y = msb(cos8u(msb(ball.angley)) as uword * height) + c64scr.setcc(x, y, 81, ball.color) - anglex+=800 - angley+=947 - color++ + ball.anglex+=800 + ball.angley+=947 + ball.color++ } } } diff --git a/examples/test.p8 b/examples/test.p8 index 4afeebb27..dd3c1498b 100644 --- a/examples/test.p8 +++ b/examples/test.p8 @@ -7,79 +7,24 @@ sub start() { - uword derp =44 - ubyte[] v = [22,33,44] - - Color foreground = [1,2,3] - c64scr.print_ub(foreground.red) - c64.CHROUT(':') - c64scr.print_ub(foreground.green) - c64.CHROUT(':') - c64scr.print_ub(foreground.blue) - c64.CHROUT('\n') - - - Color background - Color cursor = [255,255,255] - - foreground.red=99 - background.blue=foreground.red - - cursor = [1,2,3] ; assign all members at once - cursor = v - cursor = foreground ; @todo memberwise assignment - - c64scr.print_ub(foreground.red) - c64.CHROUT(':') - c64scr.print_ub(foreground.green) - c64.CHROUT(':') - c64scr.print_ub(foreground.blue) - c64.CHROUT('\n') - c64scr.print_ub(background.red) - c64.CHROUT(':') - c64scr.print_ub(background.green) - c64.CHROUT(':') - c64scr.print_ub(background.blue) - c64.CHROUT('\n') - c64scr.print_ub(cursor.red) - c64.CHROUT(':') - c64scr.print_ub(cursor.green) - c64.CHROUT(':') - c64scr.print_ub(cursor.blue) - c64.CHROUT('\n') - - foo() - foo() - foo() - foo() - foo() - foo() - foo() + Color subcol + A=msb(subcol.red) + for ubyte i in 10 to 20 { + ;A=subcol.red + ;A=blocklevelcolor.green + ;subcol.blue = Y + ;blocklevelcolor.green=Y + A=msb(subcol.red) + } return } - sub foo() { - Color localcolor - localcolor.red++ - c64scr.print_ub(localcolor.red) - c64.CHROUT(':') - c64scr.print_ub(localcolor.green) - c64.CHROUT(':') - c64scr.print_ub(localcolor.blue) - c64.CHROUT('\n') - } - struct Color { ubyte red ubyte green ubyte blue } - ; @todo structs as sub args. After strings and arrays as sub-args. -; sub foo(Color arg) -> ubyte { -; return arg.red+arg.green+arg.blue -; } - }