Compare commits

...
13 Commits
49 changed files with 1436 additions and 219 deletions

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
---
description: Startup workflow to load project context
---
1. Read the file `QWEN.md` in the project root to understand the project structure, module descriptions, and development commands.
+32
View File
@@ -0,0 +1,32 @@
# Agent Instructions for Prog8 Project
## Required Startup Actions
When starting a new session with this project, you **MUST** perform these actions before proceeding with any user requests:
1. **Read QWEN.md** - Execute: `read /home/irmen/Projects/prog8/QWEN.md`
- This file contains critical project context including:
- Project overview (Prog8 compiler for 8-bit retro systems)
- Module descriptions for all 20+ modules
- Gradle commands and build instructions
- Prog8 compiler usage examples
- Development shortcuts
2. **Check startup workflow** - Execute: `read /home/irmen/Projects/prog8/.agent/workflows/startup.md`
- This file may contain additional startup instructions
## Why This Matters
The prog8 project is a complex compiler with:
- Multiple code generation backends (6502, IR, Virtual Machine)
- 20+ interconnected modules
- Specific build and development workflows
- Proprietary language syntax and conventions
Without reading QWEN.md first, you will lack essential context needed to:
- Understand module relationships
- Use correct gradle commands
- Reference the right files
- Follow project conventions
**DO NOT PROCEED** with any user requests until you've read QWEN.md!
+86
View File
@@ -0,0 +1,86 @@
# Qwen Assistant Memory for prog8 Project
This file contains project-specific information that helps the Qwen assistant understand and work effectively with the prog8 codebase.
## Project Overview
- This project is a compiler for the Prog8 programming language.
- Prog8 is a programming language primarily targeting 8-bit retro systems with the 6502 CPU, such as the Commodore 64, Commodore 128, and Commander X16.
- The compiler has a 6502 code generator backend, and an IR code generator.
- The IR code is meant to be used in a new machine specific code generator backend (primarily 6502 but maybe 68000 as well later)
- The compiler includes a simple 'virtual machine' that can execute the IR code directly via interpretation.
- Prog8 source files have .p8 extension
- Prog8 source files are a "module" that can contain 1 ore more "blocks". They can also import other modules, from internal library files or from source files on the filesystem.
- The prog8 compiler is written mostly in Kotlin, those files have the .kt extension.
## Prog8 language feature hints
- an overview of the language features can be found in the documentation file docs/source/introduction.rst
- the syntax and grammar is specified in an ANTLR4 grammar file found in the parser directory
- the syntax for boolean logical operators is 'and', 'or', 'xor', 'not'. Bitwise operators are '&', '|', '^', '~', and '<<','>>' for bit shifting left and right respectively. All operators are documents in docs/source/programming.rst
- note that Prog8 does have a 4-byte signed long type and 5-byte "Microsoft" floating point support
- module imports are done using "%import modulename"
## Module Descriptions
- `beanshell` - EXPERIMENTAL/UNFINISHED Contains BeanShell integration for scripting capabilities within the compiler
- `benchmark-c` - C implementations for performance comparison and benchmarking
- `benchmark-program` - Benchmark programs to test compiler output performance
- `codeCore` - Core code generation utilities and shared components
- `codeGenCpu6502` - 6502 CPU-specific code generator backend (generates assembly for 6502-based systems)
- `codeGenExperimental` - Experimental code generators that are under development
- `codeGenIntermediate` - Intermediate representation (IR) code generator
- `codeGenVirtual` - Virtual machine code generator backend
- `codeOptimizers` - Optimization passes for improving generated code efficiency
- `compiler` - Main compiler executable and top-level compiler logic
- `compilerAst` - complicated Abstract Syntax Tree (AST) where most optimizations also run on, is later transformed into the simpleAst
- `docs` - Documentation files for the Prog8 language and compiler
- `examples` - Example Prog8 programs demonstrating language features
- `intermediate` - Components related to the intermediate representation (IR) of Prog8 programs
- `languageServer` - Language Server Protocol implementation for IDE integration
- `parser` - ANTLR4 Parser implementation for the Prog8 language syntax
- `scripts` - Utility scripts for development, testing, and deployment
- `simpleAst` - Simplified AST that is used to run the code generator backends from
- `syntax-files` - Syntax definition files for editors and IDEs
- `virtualmachine` - Virtual machine implementation that can execute IR code
## Key Information
- never read the files and directories that are ignored via the .aiignore and .gitignore files
- Current development focus areas: compiler optimizations, new language features, backend improvements
- Important project conventions: Kotlin for compiler implementation, modular architecture, IR-based compilation
- Architecture decisions: separation of frontend/parser, IR intermediate representation, multiple backends
### Gradle Commands
- use the system installed gradle command instead of the gradle wrapper.
- `gradle build` - Full build of the compiler including running the full test suite
- `gradle clean` - Clean build artifacts
- `gradle compileKotlin` - Compile only the Kotlin source code
- `gradle installdist` - Create the compiler JARs and executable file
- `gradle installshadowdist` - Create the single "fat" compiler JAR and executable file
### Development Shortcuts
- `gradle test --tests "*TestName*"` - Run specific test classes
- `gradle build --refresh-dependencies` - Refresh dependencies during development
### Prog8 Compiler Usage
- the prog8c compiler executable can be found in the compiler/build/install/prog8c/bin folder (this is already added to the shell's path)
- the `-emu` switch can be used to directly execute the resulting program in an emulator after successful compilation.
- `prog8c -target cx16 input.p8` - Compile a Prog8 source file "input.p8" for the CommanderX16 target
- `prog8c -target cx16 -check input.p8` - Quickly check a Prog8 source file "input.p8" for compiler errors, no output binary is produced
- `prog8c -target cx16 -emu input.p8` - Compile and execute a prog8 file in the CommanderX16 emulator
- `prog8c -target c64 -emu input.p8` - Compile and execute a prog8 file in the Commodore-64 emulator
- `prog8c -target virtual input.p8` - Compile a prog8 file for the IR/Virtual machine target
- `prog8c -target virtual -emu input.p8` - Compile and directly execute a prog8 file in the Virtual Machine
- `prog8c -vm input.p8ir` - Execute an existing prog8 program, compiled in IR form, in the Virtual Machine
- `x16emu -scale 2 -prg input.prg` - Just load an existing compiled program in the CommanderX16 emulator
- `x64sc input.prg` - run an existing compiled program in the Commodore-64 emulator (Note: You can safely ignore errors and warnings from this emulator)
### Compilation Output Files
- `*.prg` - The final compiled program file for the target system (e.g., Commander X16)
- `*.asm` - Generated assembly code from the Prog8 source
- `*.list` - Generated full assembly listing file from the Prog8 source
- `*.p8ir` - Intermediate representation file, can be executed in the Virtual Machine
- `*.vice-mon-list` - Vice emulator monitor list file for debugging
## Working Notes
[Any ongoing notes about the project that would be helpful for the assistant to remember]
## TODO Items
The file docs/source/todo.rst contains a comprehensive list of things that still have to be fixed, implemented, or optimized.
+12 -12
View File
@@ -52,18 +52,18 @@ val BaseDataType.isByteOrBool get() = this in arrayOf(BaseDataType.UBYTE, BaseDa
val BaseDataType.isWord get() = this in arrayOf(BaseDataType.UWORD, BaseDataType.WORD)
val BaseDataType.isLong get() = this == BaseDataType.LONG
val BaseDataType.isFloat get() = this == BaseDataType.FLOAT
val BaseDataType.isInteger get() = this in arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG)
val BaseDataType.isIntegerOrBool get() = this in arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.BOOL)
val BaseDataType.isWordOrByteOrBool get() = this in arrayOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.BOOL)
val BaseDataType.isInteger get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG)
val BaseDataType.isIntegerOrBool get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.BOOL)
val BaseDataType.isWordOrByteOrBool get() = this in setOf(BaseDataType.UBYTE, BaseDataType.BYTE, BaseDataType.UWORD, BaseDataType.WORD, BaseDataType.BOOL)
val BaseDataType.isNumeric get() = this == BaseDataType.FLOAT || this.isInteger
val BaseDataType.isNumericOrBool get() = this == BaseDataType.BOOL || this.isNumeric
val BaseDataType.isSigned get() = this in arrayOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
val BaseDataType.isSigned get() = this in setOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
val BaseDataType.isArray get() = this == BaseDataType.ARRAY || this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER
val BaseDataType.isPointer get() = this == BaseDataType.POINTER
val BaseDataType.isStructInstance get() = this == BaseDataType.STRUCT_INSTANCE
val BaseDataType.isPointerArray get() = this == BaseDataType.ARRAY_POINTER
val BaseDataType.isSplitWordArray get() = this == BaseDataType.ARRAY_SPLITW || this == BaseDataType.ARRAY_POINTER // pointer arrays are also always stored as split uwords
val BaseDataType.isIterable get() = this in arrayOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
val BaseDataType.isIterable get() = this in setOf(BaseDataType.STR, BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW, BaseDataType.ARRAY_POINTER)
val BaseDataType.isPassByRef get() = this.isIterable && !this.isPointer
val BaseDataType.isPassByValue get() = !this.isIterable || this.isPointer
@@ -307,14 +307,14 @@ class DataType private constructor(val base: BaseDataType, val sub: BaseDataType
infix fun isAssignableTo(targetType: DataType) =
when(base) {
BaseDataType.BOOL -> targetType.base == BaseDataType.BOOL
BaseDataType.UBYTE -> targetType.base in arrayOf(BaseDataType.UBYTE, BaseDataType.WORD, BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.BYTE -> targetType.base in arrayOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.UWORD -> targetType.base in arrayOf(BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
BaseDataType.WORD -> targetType.base in arrayOf(BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.LONG -> targetType.base in arrayOf(BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.UBYTE -> targetType.base in setOf(BaseDataType.UBYTE, BaseDataType.WORD, BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.BYTE -> targetType.base in setOf(BaseDataType.BYTE, BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.UWORD -> targetType.base in setOf(BaseDataType.UWORD, BaseDataType.LONG, BaseDataType.FLOAT, BaseDataType.POINTER, BaseDataType.ARRAY_POINTER)
BaseDataType.WORD -> targetType.base in setOf(BaseDataType.WORD, BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.LONG -> targetType.base in setOf(BaseDataType.LONG, BaseDataType.FLOAT)
BaseDataType.FLOAT -> targetType.base in arrayOf(BaseDataType.FLOAT)
BaseDataType.STR -> targetType.base in arrayOf(BaseDataType.STR, BaseDataType.UWORD) || (targetType.isPointer && targetType.sub==BaseDataType.UBYTE)
BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW -> targetType.base in arrayOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
BaseDataType.STR -> targetType.base in setOf(BaseDataType.STR, BaseDataType.UWORD) || (targetType.isPointer && targetType.sub==BaseDataType.UBYTE)
BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW -> targetType.base in setOf(BaseDataType.ARRAY, BaseDataType.ARRAY_SPLITW) && targetType.sub == sub
BaseDataType.POINTER -> {
when {
targetType.base == BaseDataType.UWORD || targetType.base == BaseDataType.LONG -> true
+5 -5
View File
@@ -1,10 +1,10 @@
package prog8.code.core
val AssociativeOperators = arrayOf("+", "*", "&", "|", "^", "==", "!=", "xor") // note: and,or are not associative because of Shortcircuit/McCarthy evaluation
val ComparisonOperators = arrayOf("==", "!=", "<", ">", "<=", ">=")
val LogicalOperators = arrayOf("and", "or", "xor", "not", "in")
val BitwiseOperators = arrayOf("&", "|", "^", "~")
val PrefixOperators = arrayOf("+", "-", "~", "not")
val AssociativeOperators = setOf("+", "*", "&", "|", "^", "==", "!=", "xor") // note: and,or are not associative because of Shortcircuit/McCarthy evaluation
val ComparisonOperators = setOf("==", "!=", "<", ">", "<=", ">=")
val LogicalOperators = setOf("and", "or", "xor", "not", "in")
val BitwiseOperators = setOf("&", "|", "^", "~")
val PrefixOperators = setOf("+", "-", "~", "not")
fun invertedComparisonOperator(operator: String) =
when (operator) {
+3 -2
View File
@@ -4,6 +4,7 @@ import prog8.code.sanitize
import prog8.code.source.SourceCode
import java.nio.file.InvalidPathException
import kotlin.io.path.Path
import kotlin.io.path.toPath
data class Position(val file: String, val line: Int, val startCol: Int, val endCol: Int) {
override fun toString(): String = "[$file: line $line col ${startCol}-${endCol}]"
@@ -21,8 +22,8 @@ data class Position(val file: String, val line: Int, val startCol: Int, val endC
if(SourceCode.isLibraryResource(file))
return "$file:$line:$startCol:"
return try {
val path = Path(file).sanitize().toString()
"file://$path:$line:$startCol:"
val path = Path(file).sanitize().toUri().toString()
"$path:$line:$startCol:"
} catch(_: InvalidPathException) {
// this can occur on Windows when the source origin contains "invalid" characters such as ':'
"file://$file:$line:$startCol:"
+2 -2
View File
@@ -19,7 +19,7 @@ data class Mflpt5(val b0: UByte, val b1: UByte, val b2: UByte, val b3: UByte, va
val flt = num.toDouble()
if (flt !in FLOAT_MAX_NEGATIVE..FLOAT_MAX_POSITIVE)
throw InternalCompilerException("floating point number out of 5-byte mflpt range: $this")
throw InternalCompilerException("floating point number out of 5-byte mflpt range: $flt")
if (flt == 0.0)
return zero
@@ -40,7 +40,7 @@ data class Mflpt5(val b0: UByte, val b1: UByte, val b2: UByte, val b3: UByte, va
return when {
exponent < 0 -> zero // underflow, use zero instead
exponent > 255 -> throw InternalCompilerException("floating point overflow: $this")
exponent > 255 -> throw InternalCompilerException("floating point overflow: $flt")
exponent == 0 -> zero
else -> {
val mantLong = mantissa.toLong()
@@ -332,8 +332,8 @@ class AsmGen6502Internal (
private var generatedLabelSequenceNumber: Int
) {
internal val optimizedByteMultiplications = arrayOf(3,5,6,7,9,10,11,12,13,14,15,20,25,40,50,80,100)
internal val optimizedWordMultiplications = arrayOf(3,5,6,7,9,10,12,15,20,25,40,50,80,100,320,640)
internal val optimizedByteMultiplications = setOf(3,5,6,7,9,10,11,12,13,14,15,20,25,40,50,80,100)
internal val optimizedWordMultiplications = setOf(3,5,6,7,9,10,12,15,20,25,40,50,80,100,320,640)
internal val loopEndLabels = ArrayDeque<String>()
private val zeropage = options.compTarget.zeropage
private val allocator = VariableAllocator(symbolTable, options, errors)
@@ -584,7 +584,7 @@ private fun optimizeJsrRtsAndOtherCombinations(linesByFour: Sequence<List<Indexe
}
else if(" cmp #0" in second || "\tcmp #0" in second) {
// there are many instructions that modify A and set the bits...
for(instr in arrayOf("lda", "ora", "and", "eor", "adc", "sbc", "asl", "cmp", "inc a", "lsr", "pla", "rol", "ror", "txa", "tya")) {
for(instr in setOf("lda", "ora", "and", "eor", "adc", "sbc", "asl", "cmp", "inc a", "lsr", "pla", "rol", "ror", "txa", "tya")) {
if(" $instr" in first || "\t$instr" in first) {
mods.add(Modification(lines[1].index, true, null))
}
@@ -554,7 +554,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" lda $variable | lsr a | bcc + | ora #$80 |+ | sta $variable")
}
else -> throw AssemblyError("weird node")
else -> throw AssemblyError("weird node $what")
}
}
BaseDataType.UWORD -> {
@@ -573,7 +573,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" lsr $variable+1 | ror $variable | bcc + | lda $variable+1 | ora #$80 | sta $variable+1 |+ ")
}
else -> throw AssemblyError("weird node")
else -> throw AssemblyError("weird node $what")
}
}
BaseDataType.LONG -> {
@@ -593,7 +593,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
sta $variable+3
+""")
}
else -> throw AssemblyError("weird node")
else -> throw AssemblyError("weird node $what")
}
}
else -> throw AssemblyError("weird type")
@@ -628,7 +628,14 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
asmgen.storeAIntoZpPointerVar(sourceName, false)
}
else -> {
TODO("ror ptr-expression ${what.position}")
asmgen.out(" php") // save Carry
asmgen.assignExpressionToVariable(what.address, "P8ZP_SCRATCH_PTR", DataType.UWORD)
asmgen.out("""
ldy #0
lda (P8ZP_SCRATCH_PTR),y
plp
ror a
sta (P8ZP_SCRATCH_PTR),y""")
}
}
}
@@ -636,7 +643,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" ror $variable")
}
else -> throw AssemblyError("weird node")
else -> throw AssemblyError("weird node $what")
}
}
BaseDataType.UWORD -> {
@@ -657,7 +664,34 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" ror $variable+1 | ror $variable")
}
else -> throw AssemblyError("weird node")
is PtPointerDeref -> {
asmgen.out(" php") // save Carry
val (zpPtrVar, offset) = ptrgen.deref(what, false)
asmgen.out("""
ldy #$offset+1
lda ($zpPtrVar),y
plp
ror a
sta ($zpPtrVar),y
dey
lda ($zpPtrVar),y
ror a
sta ($zpPtrVar),y""")
}
else -> {
asmgen.out(" php") // save Carry
asmgen.assignExpressionToVariable(what, "P8ZP_SCRATCH_PTR", DataType.UWORD)
asmgen.out("""
ldy #1
lda (P8ZP_SCRATCH_PTR),y
plp
ror a
sta (P8ZP_SCRATCH_PTR),y
dey
lda (P8ZP_SCRATCH_PTR),y
ror a
sta (P8ZP_SCRATCH_PTR),y""")
}
}
}
BaseDataType.LONG -> {
@@ -668,7 +702,9 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" ror $variable+3 | ror $variable+2 | ror $variable+1 | ror $variable")
}
else -> throw AssemblyError("weird node")
else -> {
TODO("ror long on expression $what")
}
}
}
else -> throw AssemblyError("weird type")
@@ -700,7 +736,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" lda $variable | cmp #$80 | rol a | sta $variable")
}
else -> throw AssemblyError("weird node")
else -> throw AssemblyError("weird node $what")
}
}
BaseDataType.UWORD -> {
@@ -719,7 +755,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" asl $variable | rol $variable+1 | bcc + | inc $variable |+ ")
}
else -> throw AssemblyError("weird node")
else -> throw AssemblyError("weird node $what")
}
}
BaseDataType.LONG -> {
@@ -737,7 +773,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
inc $variable
+""")
}
else -> throw AssemblyError("weird node")
else -> throw AssemblyError("weird node $what")
}
}
else -> throw AssemblyError("weird type")
@@ -771,7 +807,14 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
asmgen.storeAIntoZpPointerVar(sourceName, false)
}
else -> {
TODO("rol ptr-expression ${what.position}")
asmgen.out(" php") // save Carry
asmgen.assignExpressionToVariable(what.address, "P8ZP_SCRATCH_PTR", DataType.UWORD)
asmgen.out("""
ldy #0
lda (P8ZP_SCRATCH_PTR),y
plp
rol a
sta (P8ZP_SCRATCH_PTR),y""")
}
}
}
@@ -779,7 +822,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" rol $variable")
}
else -> throw AssemblyError("weird node")
else -> throw AssemblyError("weird node $what")
}
}
BaseDataType.UWORD -> {
@@ -800,7 +843,34 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" rol $variable | rol $variable+1")
}
else -> throw AssemblyError("weird node")
is PtPointerDeref -> {
asmgen.out(" php") // save Carry
val (zpPtrVar, offset) = ptrgen.deref(what, false)
asmgen.out("""
ldy #$offset
lda ($zpPtrVar),y
plp
rol a
sta ($zpPtrVar),y
iny
lda ($zpPtrVar),y
rol a
sta ($zpPtrVar),y""")
}
else -> {
asmgen.out(" php") // save Carry
asmgen.assignExpressionToVariable(what, "P8ZP_SCRATCH_PTR", DataType.UWORD)
asmgen.out("""
ldy #0
lda (P8ZP_SCRATCH_PTR),y
plp
rol a
sta (P8ZP_SCRATCH_PTR),y
iny
lda (P8ZP_SCRATCH_PTR),y
rol a
sta (P8ZP_SCRATCH_PTR),y""")
}
}
}
BaseDataType.LONG -> {
@@ -811,7 +881,9 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
val variable = asmgen.asmVariableName(what)
asmgen.out(" rol $variable | rol $variable+1 | rol $variable+2 | rol $variable+3")
}
else -> throw AssemblyError("weird node")
else -> {
TODO("rol long on expression $what")
}
}
}
else -> throw AssemblyError("weird type")
@@ -2361,7 +2433,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
else -> throw AssemblyError("invalid reg")
}
} else {
if(arg is PtArrayIndexer && resultRegister in arrayOf(null, RegisterOrPair.A, RegisterOrPair.Y, RegisterOrPair.X)) {
if(arg is PtArrayIndexer && resultRegister in setOf(null, RegisterOrPair.A, RegisterOrPair.Y, RegisterOrPair.X)) {
// just read the msb byte out of the word array
if(arg.splitWords) {
if(arg.variable==null)
@@ -2474,7 +2546,7 @@ internal class BuiltinFunctionsAsmGen(private val program: PtProgram,
else -> throw AssemblyError("invalid reg")
}
} else {
if(arg is PtArrayIndexer && resultRegister in arrayOf(null, RegisterOrPair.A, RegisterOrPair.Y, RegisterOrPair.X)) {
if(arg is PtArrayIndexer && resultRegister in setOf(null, RegisterOrPair.A, RegisterOrPair.Y, RegisterOrPair.X)) {
// just read the lsb byte out of the word array
if(arg.variable==null)
TODO("support for ptr indexing ${arg.position}")
@@ -204,7 +204,7 @@ internal class FunctionCallAsmGen(private val program: PtProgram, private val as
private fun usesOtherRegistersWhileEvaluating(arg: PtExpression): Boolean {
return when(arg) {
is PtBuiltinFunctionCall -> {
if (arg.name in arrayOf("lsb", "msb", "lsw", "msw"))
if (arg.name in setOf("lsb", "msb", "lsw", "msw"))
usesOtherRegistersWhileEvaluating(arg.args[0])
else if (arg.name == "mkword" || arg.name == "mklong2")
usesOtherRegistersWhileEvaluating(arg.args[0]) || usesOtherRegistersWhileEvaluating(arg.args[1])
@@ -1528,7 +1528,7 @@ $shortcutLabel:""")
}
private fun inplacemodificationRegisterAwithVariable(operator: String, variable: String, signed: Boolean) {
if(operator in "+-" && variable in arrayOf("#1", "#$1", "#$01", "#%1", "#%00000001")) {
if(operator in "+-" && variable in setOf("#1", "#$1", "#$01", "#%1", "#%00000001")) {
if(asmgen.isTargetCpu(CpuType.CPU65C02)) {
if(operator=="+")
asmgen.out(" inc a")
@@ -373,7 +373,7 @@ class ConstExprEvaluator {
}
"isspace" -> {
val char = args[0].number.toInt()
NumericLiteral.fromBoolean(char in arrayOf(32, 13, 9, 10, 141, 160), func.position)
NumericLiteral.fromBoolean(char in setOf(32, 13, 9, 10, 141, 160), func.position)
}
"isprint" -> {
val char = args[0].number.toInt()
+1 -1
View File
@@ -3,7 +3,7 @@ plugins {
kotlin("jvm")
// id("com.github.johnrengelman.shadow") version "8.1.1"
// id("io.github.goooler.shadow") version "8.1.8"
id("com.gradleup.shadow") version "9.2.2"
id("com.gradleup.shadow") version "9.3.1"
id("com.peterabeles.gversion") version "1.10.3"
}
+24
View File
@@ -651,6 +651,30 @@ _try_iso
}}
}
sub str2long(str string) -> long {
; -- convert a decimal string (terminated with a zero byte) into a long. Clobbers R0
long result = 0
bool @nozp negative = string[0] == '-'
if negative or string[0]=='+'
string++
alias digit_char = cx16.r0L
repeat {
digit_char = @(string)
if digit_char in '0' to '9' {
result = (result<<1) + (result<<3) ; multiply by 10
result += digit_char - '0' ; add digit
} else {
break ; Invalid character, stop processing
}
string++
}
if negative
return -result
else
return result
}
asmsub bin2uword(str string @AY) -> uword @AY {
; -- binary string (with or without '%') to uword.
; stops parsing at the first character that's not a 0 or 1. (except leading %)
+2
View File
@@ -423,6 +423,7 @@ io_error:
; note: only a single iteration loop can be active at a time!
; Returns true if the file is successfully opened and readable.
; No need to check status(), unlike f_open_w() !
; NOTE: may not work correctly for empty files. Try to avoid empty files on CBM DOS systems.
; NOTE: the default input isn't yet set to this logical file, you must use reset_read_channel() to do this,
; if you're going to read from it yourself instead of using f_read()!
f_close()
@@ -1076,6 +1077,7 @@ io_error:
sub exists(str filename) -> bool {
; -- returns true if the given file exists on the disk, otherwise false
; DON'T use this if you already have a file open with f_open!
; NOTE: may not work correctly for empty files. Try to avoid empty files on CBM DOS systems.
; NOTE: doesn't clear the dos error status and message, you'll have to read/clear that yourself (with status() for example)
if f_open(filename) {
f_close()
+1
View File
@@ -432,6 +432,7 @@ io_error:
sub exists(str filename) -> bool {
; -- returns true if the given file exists on the disk, otherwise false
; DON'T use this if you already have a file open with f_open!
; NOTE: may not work correctly for empty files. Try to avoid empty files on CBM DOS systems.
; NOTE: doesn't clear the dos error status and message, you'll have to read/clear that yourself (with status() for example)
cbm.SETNAM(strings.length(filename), filename)
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, 0)
@@ -331,6 +331,7 @@ close_end:
; note: only a single iteration loop can be active at a time!
; Returns true if the file is successfully opened and readable.
; No need to check status(), unlike f_open_w() !
; NOTE: may not work correctly for empty files. Try to avoid empty files on CBM DOS systems.
; NOTE: the default input isn't yet set to this logical file, you must use reset_read_channel() to do this,
; if you're going to read from it yourself instead of using f_read()!
f_close()
@@ -703,6 +704,7 @@ io_error:
sub exists(str filename) -> bool {
; -- returns true if the given file exists on the disk, otherwise false
; DON'T use this if you already have a file open with f_open!
; NOTE: may not work correctly for empty files. Try to avoid empty files on CBM DOS systems.
; NOTE: doesn't clear the dos error status and message, you'll have to read/clear that yourself (with status() for example)
if f_open(filename) {
f_close()
+25 -1
View File
@@ -249,7 +249,7 @@ sub str2uword(str string) -> uword {
}
sub str2word(str string) -> word {
; -- returns the signed word value of the string number argument in AY
; -- returns the signed word value of the string number argument
; the number may be preceded by a + or - sign but may NOT contain spaces
; (any non-digit character will terminate the number string that is parsed)
%ir {{
@@ -299,6 +299,30 @@ sub hex2long(str string) -> long {
}
}
sub str2long(str string) -> long {
; -- convert a decimal string (terminated with a zero byte) into a long. Clobbers R0
long result = 0
bool @nozp negative = string[0] == '-'
if negative or string[0]=='+'
string++
alias digit_char = cx16.r0L
repeat {
digit_char = @(string)
if digit_char in '0' to '9' {
result = (result<<1) + (result<<3) ; multiply by 10
result += digit_char - '0' ; add digit
} else {
break ; Invalid character, stop processing
}
string++
}
if negative
return -result
else
return result
}
sub bin2uword(str string) -> uword {
; -- binary string (with or without '%') to uword.
; stops parsing at the first character that's not a 0 or 1. (except leading %)
@@ -62,7 +62,7 @@ internal val constEvaluatorsForBuiltinFuncs: Map<String, ConstExpressionCaller>
)
internal fun builtinFunctionReturnType(function: String): InferredTypes.InferredType {
if(function in arrayOf("set_carry", "set_irqd", "clear_carry", "clear_irqd"))
if(function in setOf("set_carry", "set_irqd", "clear_carry", "clear_irqd"))
return InferredTypes.InferredType.void()
val func = BuiltinFunctions.getValue(function)
+1 -1
View File
@@ -27,7 +27,7 @@ internal class ErrorReporter(val colors: IConsoleColors): IErrorReporter {
override fun undefined(symbol: List<String>, suggestImport: Boolean, position: Position) {
if(suggestImport)
err("undefined symbol: ${symbol.joinToString(".")} (maybe you forgot to import the module that defines ${symbol.first()}?)", position)
err("undefined symbol: ${symbol.joinToString(".")} (maybe you forgot to import a module that defines ${symbol.first()}?)", position)
else
err("undefined symbol: ${symbol.joinToString(".")}", position)
}
@@ -312,6 +312,10 @@ internal class AstChecker(private val program: Program,
super.visit(jump)
}
override fun visit(alias: Alias) {
throw FatalAstException("all Alias nodes should have been removed from the AST ${alias.position}")
}
override fun visit(block: Block) {
if(block.name.startsWith('_'))
errors.err("identifiers cannot start with an underscore", block.position)
@@ -1221,14 +1225,14 @@ internal class AstChecker(private val program: Program,
err("this directive may only occur in a block or at module level")
if(directive.args.isEmpty())
err("missing option directive argument(s)")
else if(directive.args.map{it.string in arrayOf("enable_floats", "force_output", "no_sysinit", "merge", "verafxmuls", "no_symbol_prefixing", "ignore_unused", "romable")}.any { !it })
else if(directive.args.map{it.string in setOf("enable_floats", "force_output", "no_sysinit", "merge", "verafxmuls", "no_symbol_prefixing", "ignore_unused", "romable")}.any { !it })
err("invalid option directive argument(s)")
if(directive.parent is Block) {
if(directive.args.any {it.string !in arrayOf("force_output", "merge", "verafxmuls", "no_symbol_prefixing", "ignore_unused")})
if(directive.args.any {it.string !in setOf("force_output", "merge", "verafxmuls", "no_symbol_prefixing", "ignore_unused")})
err("using an option that is not valid for blocks")
}
if(directive.parent is Module) {
if(directive.args.any {it.string !in arrayOf("enable_floats", "no_sysinit", "no_symbol_prefixing", "ignore_unused", "romable")})
if(directive.args.any {it.string !in setOf("enable_floats", "no_sysinit", "no_symbol_prefixing", "ignore_unused", "romable")})
err("using an option that is not valid for modules")
}
if(directive.args.any { it.string=="verafxmuls" } && compilerOptions.compTarget.name != Cx16Target.NAME)
@@ -1247,7 +1251,7 @@ internal class AstChecker(private val program: Program,
for(arg in directive.args) {
val target = directive.definingScope.lookup(arg.string!!.split('.'))
if(target==null)
errors.err("undefined symbol: ${arg.string}", arg.position)
errors.undefined(listOf(arg.string!!), position = arg.position)
else if (target !is Subroutine)
errors.err("jmptable entry can only be a subroutine: ${arg.string}", arg.position)
}
@@ -1591,7 +1595,7 @@ internal class AstChecker(private val program: Program,
if(leftDt.isBool || rightDt.isBool ||
(expr.left as? TypecastExpression)?.expression?.inferType(program)?.isBool==true ||
(expr.right as? TypecastExpression)?.expression?.inferType(program)?.isBool==true) {
if(expr.operator in arrayOf("<", "<=", ">", ">=")) {
if(expr.operator in setOf("<", "<=", ">", ">=")) {
errors.err("can't use boolean operand with this comparison operator", expr.position)
}
// for now, don't enforce bool type with only logical operators...
@@ -2186,7 +2190,7 @@ internal class AstChecker(private val program: Program,
if (deref.inferType(program).isUnknown) {
val symbol = deref.definingScope.lookup(deref.chain.take(1))
if(symbol==null)
errors.err("undefined symbol: ${deref.chain[0]}", deref.position)
errors.undefined(deref.chain.take(1), position=deref.position)
else
errors.err("unable to determine type of dereferenced pointer expression", deref.position)
}
@@ -84,6 +84,12 @@ internal fun Program.charLiteralsToUByteLiterals(target: ICompilationTarget, err
}
return noModifications
}
override fun after(alias: Alias, parent: Node): Iterable<IAstModification> {
// remove all alias nodes, every aliased identifier should have been replaced in the previous step
return listOf(IAstModification.Remove(alias, parent as IStatementContainer))
}
}
walker.visit(this)
@@ -241,11 +247,13 @@ internal fun IdentifierReference.checkFunctionOrLabelExists(program: Program, st
else
errors.err("cannot call that: ${this.nameInSource.joinToString(".")}", this.position)
}
is Alias, is StructDecl, is StructFieldRef -> {
is StructFieldRef, is StructDecl, is Alias -> {
return targetStatement
}
null -> {
errors.undefined(this.nameInSource, this.firstTarget(program.builtinFunctions)==null, this.position)
val alias = definingScope.lookup(this.nameInSource.take(1))
if(alias !is Alias || alias.target.targetStatement(program.builtinFunctions)==null)
errors.undefined(this.nameInSource, this.firstTarget(program.builtinFunctions)==null, this.position)
}
else -> errors.err("cannot call that: ${this.nameInSource.joinToString(".")}", this.position)
}
@@ -41,14 +41,6 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
errors.err("invalid number of arguments: expected $expected but got $numArgs", pos)
}
override fun visit(alias: Alias) {
if(alias.target.targetStatement(program.builtinFunctions)==null)
errors.err("undefined symbol: ${alias.target.nameInSource.joinToString(".") }", alias.target.position)
if(alias.alias == alias.target.nameInSource.first())
errors.err("alias references itself", alias.position)
}
override fun visit(block: Block) {
val existing = blocks[block.name]
if(existing!=null) {
@@ -233,7 +225,7 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
}
private fun visitFunctionCall(call: IFunctionCall) {
fun check(target: Statement?, aliasDepth: Int) {
fun check(target: Statement?) {
when (target) {
is Subroutine -> {
val expectedNumberOfArgs: Int = target.parameters.size
@@ -275,12 +267,8 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
errors.err("wrong address variable datatype, expected uword", call.target.position)
}
is Alias -> {
if(aliasDepth>1000) {
errors.err("circular alias", target.position)
} else {
val actualtarget = target.target.targetStatement(program.builtinFunctions)
check(actualtarget, aliasDepth + 1)
}
val actualtarget = target.target.targetStatement(program.builtinFunctions)
check(actualtarget)
}
is StructDecl, is StructFieldRef -> {}
null -> {} // symbol error is given elsewhere
@@ -288,6 +276,6 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
}
}
check(call.target.targetStatement(program.builtinFunctions), 0)
check(call.target.targetStatement(program.builtinFunctions))
}
}
@@ -341,11 +341,51 @@ class AstPreprocessor(val program: Program,
return noModifications
}
override fun after(alias: Alias, parent: Node): Iterable<IAstModification> {
override fun before(alias: Alias, parent: Node): Iterable<IAstModification> {
// shortcut aliases that point to aliases (remove alias chains)
val tgt = alias.target.targetStatement(program.builtinFunctions)
if(tgt is Block) {
errors.err("cannot alias blocks", alias.target.position)
if(tgt==null) {
errors.undefined(alias.target.nameInSource, position = alias.target.position)
} else {
if(alias.alias == alias.target.nameInSource.first()) {
errors.err("alias loop", alias.position)
} else if(tgt is Alias) {
var chainedAlias = alias
var chainedTargetName = alias.target
var maxhops = 100
while(true) {
val tgt2 = chainedAlias.target.targetStatement(program.builtinFunctions)
if (tgt2 is Alias) {
chainedAlias = tgt2
chainedTargetName = tgt2.target
}
else {
val tgt2 = chainedTargetName.targetStatement(program.builtinFunctions) as? INamedStatement
val replacement = if(tgt2!=null) {
if(tgt2 is BuiltinFunctionPlaceholder) {
val unscopedTarget = IdentifierReference(listOf(tgt2.name), alias.position)
Alias(alias.alias, unscopedTarget, alias.position)
} else if(tgt2.scopedName != chainedTargetName.nameInSource) {
val scopedTarget = IdentifierReference(tgt2.scopedName, alias.position)
Alias(alias.alias, scopedTarget, alias.position)
} else {
Alias(alias.alias, chainedTargetName, alias.position)
}
} else {
Alias(alias.alias, chainedTargetName, alias.position)
}
return listOf(IAstModification.ReplaceNode(alias, replacement, parent))
}
maxhops--
if(maxhops==0) {
errors.err("alias loop", alias.position)
break
}
}
}
}
return noModifications
}
@@ -23,16 +23,11 @@ internal class CodeDesugarer(val program: Program, private val target: ICompilat
// - pointer[word] replaced by @(pointer+word)
// - @(&var) and @(&var+1) replaced by lsb(var) and msb(var) if var is a word
// - flatten chained assignments
// - remove alias nodes
// - convert on..goto/call to jumpaddr array and separate goto/call
// - replace implicit pointer dereference chains (a.b.c.d) with explicit ones (a^^.b^^.c^^.d)
// - replace ptr^^ by @(ptr) if ptr is just an uword.
// - replace p1^^ = p2^^ by memcopy.
override fun after(alias: Alias, parent: Node): Iterable<IAstModification> {
return listOf(IAstModification.Remove(alias, parent as IStatementContainer))
}
override fun before(breakStmt: Break, parent: Node): Iterable<IAstModification> {
fun jumpAfter(stmt: Statement): Iterable<IAstModification> {
val label = program.makeLabel("after", breakStmt.position)
@@ -281,8 +276,6 @@ _after:
val indexExpr = arrayIndexedExpression.indexer.indexExpr
val arrayVar = arrayIndexedExpression.plainarrayvar!!.targetVarDecl()
if(arrayVar!=null && (arrayVar.datatype.isUnsignedWord || arrayVar.datatype.isPointer)) {
if(parent is IFunctionCall && parent.target.nameInSource.singleOrNull() in InplaceModifyingBuiltinFunctions)
return noModifications
val wordIndex = TypecastExpression(indexExpr, DataType.UWORD, true, indexExpr.position)
val address = BinaryExpression(
arrayIndexedExpression.plainarrayvar!!.copy(),
@@ -572,10 +565,6 @@ _after:
val firstDt = firstTarget?.datatype
if (firstDt?.isPointer == true) {
// the a.b.c.d can be a pointer dereference chain ending in a struct field; a^^.b^^.c^^.d
// don't change aliases!
if(parent is Alias)
return noModifications
val chain = mutableListOf(identifier.nameInSource[0])
var struct = firstDt.subType
for(name in identifier.nameInSource.drop(1)) {
@@ -126,18 +126,8 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
return noModifications
}
override fun after(alias: Alias, parent: Node): Iterable<IAstModification> {
val target = alias.target.targetStatement()
if(target is Alias) {
// shortcut the alias that refers to another alias
val newAlias = Alias(alias.alias, target.target, alias.position)
return listOf(IAstModification.ReplaceNode(alias, newAlias, parent))
}
return noModifications
}
override fun after(identifier: IdentifierReference, parent: Node): Iterable<IAstModification> {
val target = identifier.targetStatement()
val target = identifier.targetStatement(program.builtinFunctions)
if(target is StructFieldRef) {
// replace a.b.c.d by a^^.b^^.c^^.d
@@ -153,56 +143,45 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
// maybe the first component of the scoped name is an alias?
val tgt2 = identifier.definingScope.lookup(identifier.nameInSource.take(1)) as? Alias
if(tgt2!=null && parent !is Alias) {
if(tgt2.target.targetStatement() !is Alias) {
if(tgt2.target.targetStatement(program.builtinFunctions) !is Alias) {
val actual = IdentifierReference(tgt2.target.nameInSource + identifier.nameInSource.drop(1), identifier.position)
return listOf(IAstModification.ReplaceNode(identifier, actual, parent))
}
}
}
// don't replace an identifier in an Alias or when the alias points to another alias (that will be resolved first elsewhere)
if(target is Alias && parent !is Alias) {
val targetStatement = target.target.targetStatement()
if (targetStatement !is Alias) {
val replacement: IdentifierReference =
when (targetStatement) {
is StructFieldRef -> {
// replace with target struct field reference
target.target.copy(position = identifier.position)
}
is VarDecl -> {
val scoped = if (targetStatement.names.isNotEmpty()) {
// points to a vardecl of multiple names in 1 statement
(targetStatement.parent as INamedStatement).scopedName + target.target.nameInSource.last()
} else {
(targetStatement as INamedStatement).scopedName
}
require(scoped.last() != "<multiple>")
IdentifierReference(scoped, identifier.position)
}
else -> {
// replace with scoped identifier
val scoped = (targetStatement as INamedStatement).scopedName
require(scoped.last() != "<multiple>")
IdentifierReference(scoped, identifier.position)
}
if(target is Alias && parent !is Alias) { // don't replace an identifier in an Alias
val targetStatement = target.target.targetStatement(program.builtinFunctions)
val replacement: IdentifierReference =
when (targetStatement) {
is StructFieldRef -> {
// replace with target struct field reference
target.target.copy(position = identifier.position)
}
is VarDecl -> {
val scoped = if (targetStatement.names.isNotEmpty()) {
// points to a vardecl of multiple names in 1 statement
(targetStatement.parent as INamedStatement).scopedName + target.target.nameInSource.last()
} else {
(targetStatement as INamedStatement).scopedName
}
require(scoped.last() != "<multiple>")
IdentifierReference(scoped, identifier.position)
}
is BuiltinFunctionPlaceholder -> {
// replace with unscoped identifier
IdentifierReference(listOf(targetStatement.name), identifier.position)
}
else -> {
// replace with scoped identifier
val scoped = (targetStatement as INamedStatement).scopedName
require(scoped.last() != "<multiple>")
IdentifierReference(scoped, identifier.position)
}
}
return listOf(IAstModification.ReplaceNode(identifier, replacement, parent))
}
return listOf(IAstModification.ReplaceNode(identifier, replacement, parent))
}
// experimental code to be able to alias blocks too:
// if(target is INamedStatement) {
// if (identifier.nameInSource != target.scopedName) {
// val blockAlias = identifier.definingScope.lookup(identifier.nameInSource.take(1))
// if(blockAlias is Alias) {
// val newname = mutableListOf(blockAlias.target.nameInSource.single())
// newname.addAll(identifier.nameInSource.drop(1))
// return listOf(IAstModification.ReplaceNode(identifier, IdentifierReference(newname, position = identifier.position), parent))
// }
// }
// }
return noModifications
}
@@ -212,7 +191,7 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
val rightIndex = expr.right as? ArrayIndexedExpression
if (leftIdent != null && rightIndex != null) {
// maybe recombine IDENTIFIER . ARRAY[IDX] --> COMBINEDIDENTIFIER[IDX]
val leftTarget = leftIdent.targetStatement()
val leftTarget = leftIdent.targetStatement(program.builtinFunctions)
if(leftTarget==null || leftTarget !is StructDecl) {
if(rightIndex.plainarrayvar!=null) {
val combinedName = leftIdent.nameInSource + rightIndex.plainarrayvar!!.nameInSource
@@ -228,17 +207,12 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
return noModifications
}
override fun after(deref: ArrayIndexedPtrDereference, parent: Node): Iterable<IAstModification> {
// TODO handle aliases?
return noModifications
}
override fun after(deref: PtrDereference, parent: Node): Iterable<IAstModification> {
// handle aliases
// maybe the first component of the dereference chain is an alias?
val tgt2 = deref.definingScope.lookup(deref.chain.take(1)) as? Alias
if(tgt2!=null && parent !is Alias) {
if(tgt2.target.targetStatement() !is Alias) {
if(tgt2.target.targetStatement(program.builtinFunctions) !is Alias) {
val unaliased = PtrDereference(tgt2.target.nameInSource + deref.chain.drop(1), deref.derefLast, deref.position)
return listOf(IAstModification.ReplaceNode(deref, unaliased, parent))
}
@@ -52,7 +52,7 @@ class SimplifiedAstMaker(private val program: Program, private val errors: IErro
is ChainedAssignment -> throw FatalAstException("ChainedAssignment should have been flattened")
is Assignment -> transform(statement)
is Block -> transform(statement)
is Alias -> throw FatalAstException("alias should have been desugared")
is Alias -> throw FatalAstException("alias should have been removed")
is Break -> throw FatalAstException("break should have been replaced by Goto")
is Continue -> throw FatalAstException("continue should have been replaced by Goto")
is BuiltinFunctionPlaceholder -> throw FatalAstException("BuiltinFunctionPlaceholder should not occur in Ast here")
@@ -360,7 +360,7 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
if (rightConstVal?.number == 1.0) {
if (rightDt != leftDt && !(leftDt.isPointer && rightDt.isUnsignedWord)) {
val dt = if(leftDt.isPointer) BaseDataType.UWORD else leftDt.base
if(!dt.isLong) {
if(!dt.isLong && dt!=BaseDataType.UNDEFINED) {
val right = NumericLiteral(dt, rightConstVal.number, rightConstVal.position)
return listOf(IAstModification.ReplaceNode(expr.right, right, expr))
}
@@ -369,7 +369,7 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
else if (rightConstVal?.number == 0.0) {
if (rightDt != leftDt && !(leftDt.isPointer && rightDt.isUnsignedWord)) {
val dt = if(leftDt.isPointer) BaseDataType.UWORD else leftDt.base
if(!dt.isLong) {
if(!dt.isLong && dt!=BaseDataType.UNDEFINED) {
val right = NumericLiteral(dt, rightConstVal.number, rightConstVal.position)
return listOf(IAstModification.ReplaceNode(expr.right, right, expr))
}
@@ -386,7 +386,7 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
if (rightConstVal?.number == 1.0) {
if(rightDt!=leftDt && !(leftDt.isPointer && rightDt.isUnsignedWord)) {
val dt = if(leftDt.isPointer) BaseDataType.UWORD else leftDt.base
if(!dt.isLong) {
if(!dt.isLong && dt!=BaseDataType.UNDEFINED) {
val right = NumericLiteral(dt, rightConstVal.number, rightConstVal.position)
return listOf(IAstModification.ReplaceNode(expr.right, right, expr))
}
@@ -395,7 +395,7 @@ internal class VariousCleanups(val program: Program, val errors: IErrorReporter,
else if (rightConstVal?.number == 0.0) {
if(rightDt!=leftDt && !(leftDt.isPointer && rightDt.isUnsignedWord)) {
val dt = if(leftDt.isPointer) BaseDataType.UWORD else leftDt.base
if(!dt.isLong) {
if(!dt.isLong && dt!=BaseDataType.UNDEFINED) {
val right = NumericLiteral(dt, rightConstVal.number, rightConstVal.position)
return listOf(IAstModification.ReplaceNode(expr.right, right, expr))
}
+4 -4
View File
@@ -85,10 +85,10 @@ class TestNumbers: FunSpec({
Mflpt5.fromNumber(1.7e-39) shouldBe Mflpt5(0x00u, 0x00u, 0x00u, 0x00u, 0x00u)
Mflpt5.fromNumber(-1.7e-38) shouldBe Mflpt5(0x03u, 0xb9u, 0x1du, 0x15u, 0x63u)
Mflpt5.fromNumber(-1.7e-39) shouldBe Mflpt5(0x00u, 0x00u, 0x00u, 0x00u, 0x00u)
shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(1.7014118346e+38) }
shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(-1.7014118346e+38) }
shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(1.7014118347e+38) }
shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(-1.7014118347e+38) }
shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(1.7014118346e+38) }.message shouldContain "mflpt range: 1.7014118346E38"
shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(-1.7014118346e+38) }.message shouldContain "mflpt range: -1.7014118346E38"
shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(1.7014118347e+38) }.message shouldContain "mflpt range: 1.7014118347E38"
shouldThrow<InternalCompilerException> { Mflpt5.fromNumber(-1.7014118347e+38) }.message shouldContain "mflpt range: -1.7014118347E38"
}
test("testMflpt5ToFloat") {
+2 -2
View File
@@ -2432,7 +2432,7 @@ main {
sub alias1() {
alias TheNode = structdefs.Node
^^TheNode node = 20000
^^TheNode @shared node = 20000
node.value = 100
}
@@ -2444,7 +2444,7 @@ main {
sub alias3() {
alias TheNode = structdefs.Node
^^TheNode node = 20000
^^TheNode @shared node = 20000
node++
}
+12 -1
View File
@@ -802,12 +802,23 @@ main {
notdefined -> bar = 1
else -> bar = 2
}
bool z = undefined != 0
bool z2 = undefined == 0
if undefined==0
z= true
if undefined!=0
z= false
}
}"""
val errors = ErrorReporterForTests()
compileText(C64Target(), false, src, outputDir, writeAssembly = false, errors = errors) shouldBe null
errors.errors.size shouldBe 2
errors.errors.size shouldBe 10
errors.errors[1] shouldContain "undefined symbol"
errors.errors[2] shouldContain "undefined symbol"
errors.errors[4] shouldContain "undefined symbol"
errors.errors[6] shouldContain "undefined symbol"
errors.errors[8] shouldContain "undefined symbol"
}
test("return unsigned values for signed results ok if value fits") {
+34 -3
View File
@@ -146,10 +146,11 @@ main {
}"""
val errors = ErrorReporterForTests()
compileText(VMTarget(), false, src, outputDir, writeAssembly=false, errors = errors) shouldBe null
errors.errors.size shouldBe 3
errors.errors[0] shouldContain "references itself"
errors.errors.size shouldBe 4
errors.errors[0] shouldContain "alias loop"
errors.errors[1] shouldContain "undefined symbol: xx.yy"
errors.errors[2] shouldContain "references itself"
errors.errors[2] shouldContain "alias loop"
errors.errors[3] shouldContain "alias loop"
}
test("alias scopes") {
@@ -264,6 +265,36 @@ main {
errors.errors[4] shouldContain "invalid number of arguments: expected 1 but got 0"
errors.errors[5] shouldContain "invalid number of arguments: expected 2 but got 0"
}
xtest("chained aliasing") {
val src="""
main {
sub start() {
alias s1 = testblock.sub1
alias s2 = s1.sub2 ; TODO fix alias error
alias vx = s2.var2 ; TODO fix alias error
vx = 999
alias tb = testblock
alias vv = tb.variable ; TODO fix alias error
vv = 99
}
}
testblock {
ubyte @shared variable
sub sub1() {
sub sub2() {
ubyte @shared var2
}
}
}"""
val errors = ErrorReporterForTests()
compileText(C64Target(), optimize=false, src, outputDir, writeAssembly=false, errors=errors) shouldNotBe null
errors.errors.size shouldBe 0
}
}
context("strings") {
+21
View File
@@ -585,4 +585,25 @@ main {
branches.size shouldBeLessThanOrEqual 1
}
}
xtest("deeply scoped variable references") {
val src= $"""
main {
sub start() {
main.sub1.sub2.sub3.variable = 100
}
sub sub1() {
sub sub2() {
sub sub3() {
ubyte @shared variable
}
}
}
}"""
compileText(Cx16Target(), true, src, outputDir) shouldNotBe null
val result = compileText(VMTarget(), true, src, outputDir)!!
val virtfile = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".p8ir")
VmRunner().runProgram(virtfile.readText(), true) // TODO fix vm crash on deeply scoped symbol ref
}
})
+16 -16
View File
@@ -84,6 +84,13 @@ interface IStatementContainer {
when(stmt) {
is VarDecl -> if(stmt.name==name || stmt.names.contains(name)) return stmt
is INamedStatement -> if(stmt.name==name) return stmt
is When -> {
stmt.choices.forEach {
val found = it.statements.searchSymbol(name)
if(found!=null)
return found
}
}
is AnonymousScope -> {
val found = stmt.searchSymbol(name)
if(found!=null)
@@ -94,37 +101,30 @@ interface IStatementContainer {
if(found!=null)
return found
}
is ConditionalBranch -> {
val found = stmt.truepart.searchSymbol(name) ?: stmt.elsepart.searchSymbol(name)
if(found!=null)
return found
}
is ForLoop -> {
val found = stmt.body.searchSymbol(name)
if(found!=null)
return found
}
is WhileLoop -> {
val found = stmt.body.searchSymbol(name)
if(found!=null)
return found
}
is RepeatLoop -> {
val found = stmt.body.searchSymbol(name)
if(found!=null)
return found
}
is WhileLoop -> {
val found = stmt.body.searchSymbol(name)
if(found!=null)
return found
}
is UntilLoop -> {
val found = stmt.body.searchSymbol(name)
if(found!=null)
return found
}
is When -> {
stmt.choices.forEach {
val found = it.statements.searchSymbol(name)
if(found!=null)
return found
}
is ConditionalBranch -> {
val found = stmt.truepart.searchSymbol(name) ?: stmt.elsepart.searchSymbol(name)
if(found!=null)
return found
}
is Alias -> if(stmt.alias==name) return stmt
else -> {
@@ -654,8 +654,7 @@ class NumericLiteral(val type: BaseDataType, // only numerical types allowed
BaseDataType.LONG -> { /*no hard requirement, the range will be checked later to give a proper error message instead of a crash */ }
BaseDataType.BOOL -> require(number==0.0 || number==1.0)
BaseDataType.POINTER -> throw FatalAstException("pointer literals should not be created, should have been UWORD $position")
else -> require(type.isNumericOrBool) {
"numeric literal type should be numeric or bool: $type" }
else -> require(type.isNumericOrBool) {"numeric literal type should be numeric or bool: $type at $position" }
}
if(type!=BaseDataType.FLOAT) {
if(truncate(number) != number)
@@ -1769,27 +1768,26 @@ class PtrDereference(
fun resultType(dt: DataType?) = if(dt==null) InferredTypes.unknown() else InferredTypes.knownFor(if(derefLast) dt.dereference() else dt)
val target = definingScope.lookup(chain) ?: return InferredTypes.unknown()
if(target is VarDecl)
return resultType(target.datatype)
if(target is StructFieldRef)
return resultType(target.type)
if(target is Alias)
return target.target.inferType(program)
TODO("infertype $chain -> $target")
return when (target) {
is VarDecl -> resultType(target.datatype)
is StructFieldRef -> resultType(target.type)
is Alias -> target.target.inferType(program)
else -> TODO("infertype $chain -> $target")
}
}
override fun replaceChildNode(node: Node, replacement: Node) =
throw FatalAstException("can't replace here")
override fun referencesIdentifier(nameInSource: List<String>) = chain.size==1 && chain==nameInSource
fun isSamePointerDeref(other: Expression?): Boolean {
if(other==null || other !is PtrDereference)
return false
if(derefLast != other.derefLast)
return false
if(chain != other.chain)
return false
return true
return if(other==null || other !is PtrDereference)
false
else if(derefLast != other.derefLast)
false
else if(chain != other.chain)
false
else
true
}
fun firstTarget(): Statement? = definingScope.lookup(chain.take(1))
@@ -1,5 +1,5 @@
Prog8 compiler v12.1 by Irmen de Jong (irmen@razorvine.net)
Prog8 compiler v12.1.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-c128.p8
@@ -121,6 +121,7 @@ conv {
internal_uword2decimal (uword value @AY) -> ubyte @Y, ubyte @A, ubyte @X
internal_uword2hex (uword value @AY) clobbers (A,Y)
str2byte (str string @AY) clobbers (Y) -> byte @A
str2long (str string) -> long
str2ubyte (str string @AY) clobbers (Y) -> ubyte @A
str2uword (str string @AY) -> uword @AY
str2word (str string @AY) -> word @AY
@@ -1,5 +1,5 @@
Prog8 compiler v12.1 by Irmen de Jong (irmen@razorvine.net)
Prog8 compiler v12.1.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-c64.p8
@@ -121,6 +121,7 @@ conv {
internal_uword2decimal (uword value @AY) -> ubyte @Y, ubyte @A, ubyte @X
internal_uword2hex (uword value @AY) clobbers (A,Y)
str2byte (str string @AY) clobbers (Y) -> byte @A
str2long (str string) -> long
str2ubyte (str string @AY) clobbers (Y) -> ubyte @A
str2uword (str string @AY) -> uword @AY
str2word (str string @AY) -> word @AY
@@ -1,5 +1,5 @@
Prog8 compiler v12.1 by Irmen de Jong (irmen@razorvine.net)
Prog8 compiler v12.1.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-cx16.p8
@@ -159,6 +159,7 @@ conv {
internal_uword2decimal (uword value @AY) -> ubyte @Y, ubyte @A, ubyte @X
internal_uword2hex (uword value @AY) clobbers (A,Y)
str2byte (str string @AY) clobbers (Y) -> byte @A
str2long (str string) -> long
str2ubyte (str string @AY) clobbers (Y) -> ubyte @A
str2uword (str string @AY) -> uword @AY
str2word (str string @AY) -> word @AY
@@ -1,5 +1,5 @@
Prog8 compiler v12.1 by Irmen de Jong (irmen@razorvine.net)
Prog8 compiler v12.1.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-pet32.p8
@@ -121,6 +121,7 @@ conv {
internal_uword2decimal (uword value @AY) -> ubyte @Y, ubyte @A, ubyte @X
internal_uword2hex (uword value @AY) clobbers (A,Y)
str2byte (str string @AY) clobbers (Y) -> byte @A
str2long (str string) -> long
str2ubyte (str string @AY) clobbers (Y) -> ubyte @A
str2uword (str string @AY) -> uword @AY
str2word (str string @AY) -> word @AY
@@ -1,5 +1,5 @@
Prog8 compiler v12.1 by Irmen de Jong (irmen@razorvine.net)
Prog8 compiler v12.1.1 by Irmen de Jong (irmen@razorvine.net)
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
Compiling program import-all-virtual.p8
@@ -94,6 +94,7 @@ conv {
internal_str_ub (ubyte value, str out_ptr)
internal_str_uw (uword value, str out_ptr)
str2byte (str string) -> byte
str2long (str string) -> long
str2ubyte (str string) -> ubyte
str2uword (str string) -> uword
str2word (str string) -> word
+2 -1
View File
@@ -463,7 +463,7 @@ conv
Routines to convert strings to numbers or vice versa.
- numbers to strings, in various formats (binary, hex, decimal)
- strings in decimal, hex and binary format into numbers (bytes, words)
- strings in decimal, hex and binary format into numbers (bytes, words, longs)
Read the :source:`conv source code <compiler/res/prog8lib/conv.p8>`
to see what's in there.
@@ -586,6 +586,7 @@ to see what's in there. (Note: slight variations for different compiler targets)
Error handling is peculiar on CBM dos systems (C64, C128, cx16, PET). Read the
descriptions for the various methods in this library for details and tips.
PET support is limited, not all routines are available for this system.
Also there are some weird cases in CBM dos when dealing with empty (0-byte) files. It's best to avoid such files altogether.
emudbg (cx16 only)
+4 -2
View File
@@ -125,8 +125,10 @@ The ``alias`` statement makes it easier to refer to symbols from other places, a
you from having to type the fully scoped name everytime you need to access that symbol.
Aliases can be created in any scope except at the module level.
An alias is created with ``alias <name> = <target>`` and then you can use ``<name>`` as if it were ``<target>``.
It is possible to alias variables, labels and subroutines, but not whole blocks.
The name has to be an unscoped identifier name, the target can be any symbol.
It is possible to alias variables, labels and subroutines, and even whole blocks.words
The name has to be an unscoped identifier name, the target can be any scoped or unscoped symbol.
Please consider using aliases sparingly because it may lead to confusing code if you alias
well-known block names for example.
.. _blocks:
+7 -2
View File
@@ -1,6 +1,11 @@
TODO
====
Known bugs:
- fix chained aliasing errors see test "chained aliasing"
- fix crash in ir loader/vm for deeply nested symbol reference, see test "deeply scoped variable references"
Weird Heisenbug
^^^^^^^^^^^^^^^
- BUG: examples/cube3d-float crashes with div by zero error on C64 (works on cx16. ALready broken in v11, v10 still worked)
@@ -10,10 +15,10 @@ Weird Heisenbug
Future Things and Ideas
^^^^^^^^^^^^^^^^^^^^^^^
- make swap() a special syntax node with assigntarget arg types instead of a builtin function call (see swap-statement branch)
- make builtin functions capable of returning multiple values, then make divmod() return the 2 results rather than accepting 2 extra variables as arguments
- then also introduce lmh(longvalue) -or whatever sensible name- builtin function that returns the low, mid, hi (bank) bytes of a long.
- and rewrite the divmod (and others?) function to just return the 2 results instead of taking target variables as arguments.
- allow alias to blocks?
- add a -profile option (for now X16 only) that instruments the start (and returns?) -of every prog8 subroutine with code that dumps to the X16 emulator debug console: name of sub, stack pointer (for call depth!), emudbg cycle count. Save/restore all used registers! Start of program must set cycle count to zero.
- add @private to variables and subroutines declared in a scope to make them invisible from outside that scope?
- when implementing unsigned longs: remove the (multiple) "TODO "hack" to allow unsigned long constants to be used as values for signed longs, without needing a cast"
@@ -31,7 +36,6 @@ Future Things and Ideas
- should we have a SourceStorageKind.POINTER? (there is one for TargetStorageKind...)
- make memory mapped variables support more constant expressions such as: &uword MyHigh = &mylong1+2 (see github issue #192)
- allow memory() to occur in array initializer (maybe needed for 2 dimensional arrays?) i.e. make it a constant (see github issue #192)
- handle Alias in a general way in LiteralsToAutoVarsAndRecombineIdentifiers instead of replacing it scattered over multiple functions
- allow the value of a memory mapped variable to be address-of another variable, not just a constant number
- implement for loops with long loopvar over long range expression
- Make all constants long by default? or not? (remove type name altogether), reduce to target type implictly if the actual value fits. Experiment is in branch 'long-consts'
@@ -111,6 +115,7 @@ IR/VM
Libraries
---------
- diskio: make f_open (and f_open_w?) and exists to read the command channel for success status rather than depending on flaky ST
- Add split-word array sorting routines to sorting module?
- make a list of all floats.* routines that the compiler expects for full float support?
+2 -1
View File
@@ -324,7 +324,7 @@ Arrays
.. index:: pair: Data Types; Arrays
Arrays can be created from a list of booleans, bytes, words, floats, addresses of other variables
(such as explicit address-of expressions, strings, or other array variables), and struct initializers.
(such as explicit address-of expressions, strings, or other array variables), pointers, or struct initializers.
The values in an array literal always have to be constants.
A trailing comma is allowed, sometimes this is easier when copying values
or when adding more stuff to the array later. Here are some examples of arrays::
@@ -336,6 +336,7 @@ or when adding more stuff to the array later. Here are some examples of arrays::
str[] names = ["ally", "pete"] ; array of string pointers/addresses (equivalent to array of uwords)
uword[] others = [names, array] ; array of pointers/addresses to other arrays
bool[2] flags = [true, false] ; array of two boolean values (take up 1 byte each, like a byte array)
^^float[3] values ; array of three pointers to floats
value = array[3] ; the fourth value in the array (index is 0-based)
char = string[4] ; the fifth character (=byte) in the string
+152 -10
View File
@@ -1,17 +1,159 @@
%import textio
%import conv
%import strings
%zeropage basicsafe
main {
; Test the routine
sub start() {
bool ans
if true {
ans = false
if (ans == true) {
return
} else {
goto done
}
}
done:
str test_string1 = "12345"
str test_string2 = "-98765"
str test_string3 = "0"
str test_string4 = "2147483647" ; Max positive 32-bit signed integer
str test_string5 = "-2147483648" ; Min negative 32-bit signed integer
str test_string6 = "42"
str test_string7 = "1000000000" ; Large number
str test_string8 = "" ; Empty string
str test_string9 = "abc123" ; Invalid string
str test_string10 = "255" ; Max unsigned byte
str test_string11 = "127" ; Max signed byte
str test_string12 = "-128" ; Min signed byte
str test_string13 = "65535" ; Max unsigned word
str test_string14 = "32767" ; Max signed word
str test_string15 = "-32768" ; Min signed word
str test_string16 = "100000" ; Just over 65535/10
str test_string17 = "999999" ; Larger number
str test_string18 = "+999999" ; Larger number with plus
long result1 = conv.str2long(test_string1)
long result2 = conv.str2long(test_string2)
long result3 = conv.str2long(test_string3)
long result4 = conv.str2long(test_string4)
long result5 = conv.str2long(test_string5)
long result6 = conv.str2long(test_string6)
long result7 = conv.str2long(test_string7)
long result8 = conv.str2long(test_string8) ; This should return 0 for empty string
long result9 = conv.str2long(test_string9) ; This should return 0 since it starts with non-digit
long result10 = conv.str2long(test_string10) ; Max unsigned byte
long result11 = conv.str2long(test_string11) ; Max signed byte
long result12 = conv.str2long(test_string12) ; Min signed byte
long result13 = conv.str2long(test_string13) ; Max unsigned word
long result14 = conv.str2long(test_string14) ; Max signed word
long result15 = conv.str2long(test_string15) ; Min signed word
long result16 = conv.str2long(test_string16) ; Just over 65535/10
long result17 = conv.str2long(test_string17) ; Larger number
long result18 = conv.str2long(test_string18) ; Larger number with +
; Print results using the built-in textio library
txt.print("str ")
txt.print(test_string1)
txt.print(" = ")
txt.print_l(result1)
txt.nl()
txt.print("str ")
txt.print(test_string2)
txt.print(" = ")
txt.print_l(result2)
txt.nl()
txt.print("str ")
txt.print(test_string3)
txt.print(" = ")
txt.print_l(result3)
txt.nl()
txt.print("str ")
txt.print(test_string4)
txt.print(" = ")
txt.print_l(result4)
txt.nl()
txt.print("str ")
txt.print(test_string5)
txt.print(" = ")
txt.print_l(result5)
txt.nl()
txt.print("str ")
txt.print(test_string6)
txt.print(" = ")
txt.print_l(result6)
txt.nl()
txt.print("str ")
txt.print(test_string7)
txt.print(" = ")
txt.print_l(result7)
txt.nl()
txt.print("str ")
txt.print(test_string8)
txt.print(" = ")
txt.print_l(result8)
txt.nl()
txt.print("str ")
txt.print(test_string9)
txt.print(" = ")
txt.print_l(result9)
txt.nl()
txt.print("str ")
txt.print(test_string10)
txt.print(" = ")
txt.print_l(result10)
txt.nl()
txt.print("str ")
txt.print(test_string11)
txt.print(" = ")
txt.print_l(result11)
txt.nl()
txt.print("str ")
txt.print(test_string12)
txt.print(" = ")
txt.print_l(result12)
txt.nl()
txt.print("str ")
txt.print(test_string13)
txt.print(" = ")
txt.print_l(result13)
txt.nl()
txt.print("str ")
txt.print(test_string14)
txt.print(" = ")
txt.print_l(result14)
txt.nl()
txt.print("str ")
txt.print(test_string15)
txt.print(" = ")
txt.print_l(result15)
txt.nl()
txt.print("str ")
txt.print(test_string16)
txt.print(" = ")
txt.print_l(result16)
txt.nl()
txt.print("str ")
txt.print(test_string17)
txt.print(" = ")
txt.print_l(result17)
txt.nl()
txt.print("str ")
txt.print(test_string18)
txt.print(" = ")
txt.print_l(result18)
txt.nl()
; To make sure the program finishes properly
return
}
}
+1 -1
View File
@@ -4,4 +4,4 @@ org.gradle.parallel=true
org.gradle.daemon=true
org.gradle.configuration-cache=false
kotlin.code.style=official
version=12.1
version=12.1.1
+748
View File
@@ -0,0 +1,748 @@
# ANTLR4 Grammar Improvements for Prog8
## Overview
This document outlines recommended improvements to the `Prog8ANTLR.g4` grammar file, focusing on clearer parse errors, rule optimization, and maintainability.
## Current Issues Summary
The Prog8 ANTLR4 grammar has several areas that could be improved:
1. **Poor Error Recovery**: Uses `BailErrorStrategy` that fails immediately on first error
2. **Generic Error Messages**: Users receive cryptic ANTLR default messages
3. **Complex Expression Rule**: 25+ alternatives in a single left-recursive rule
4. **Fragile Lexer Rules**: `NOT_IN` token depends on whitespace
5. **Manual Keyword Handling**: Must explicitly list keywords that can be identifiers
6. **"Cursed" Pointer Dereference**: Complex grammar that doesn't handle chaining well
---
## 1. Error Recovery & Reporting Improvements
### Current Issues
- **BailErrorStrategy**: Fails immediately on first error with no recovery
- **Generic error messages**: ANTLR default messages like "mismatched input" are unhelpful
- **No rule-specific error messages**: Users get cryptic errors for common mistakes
### Recommended Improvements
#### Add Error Alternatives with Custom Messages
```antlr
// Assignment rule with error detection
assignment
: assign_target '=' expression
| assign_target '=' assignment
| multi_assign_target '=' expression
| assign_target '=' expression '=' expression+ // ERROR: chained assignment
{ notifyErrorListeners("Cannot chain assignments. Use multiple statements instead."); }
;
```
#### Add Error Recovery Rules
```antlr
statement
: // ... valid statements
| 'if' expression statement // Missing THEN or block
{ notifyErrorListeners("Expected 'then' or '{' after if condition"); }
| 'for' identifier 'in' // Missing expression
{ notifyErrorListeners("Expected expression after 'in'"); }
| 'while' '}' // Missing condition
{ notifyErrorListeners("Expected condition after 'while'"); }
;
```
#### Improve Error Messages for Common Mistakes
```antlr
vardecl
: datatype (arrayindex | EMPTYARRAYSIG)? TAG* identifierlist
| datatype EMPTYARRAYSG identifierlist
{ if (!$datatype.text.equals("ubyte") && !$datatype.text.equals("byte"))
notifyErrorListeners("Empty array syntax [] only valid for byte/ubyte types"); }
;
```
---
## 2. Rule Optimization
### A. Expression Rule Refactoring
**Current Problem**: Massive left-recursive rule with 25+ alternatives (lines 202-233)
**Optimization**: Group by precedence using sub-rules:
```antlr
expression
: primaryExpression
| expression postfixOperator
| prefixOperator expression
| expression multiplicativeOp expression
| expression additiveOp expression
| expression shiftOp expression
| expression relationalOp expression
| expression equalityOp expression
| expression bitwiseAndOp expression
| expression bitwiseXorOp expression
| expression bitwiseOrOp expression
| expression rangeOp expression
| expression 'in' expression
| expression 'not' 'in' expression
| expression 'and' expression
| expression 'or' expression
| expression 'xor' expression
| 'if' expression 'then' expression 'else' expression // if-expression
;
// Operator groups
multiplicativeOp: '*' | '/' | '%' ;
additiveOp: '+' | '-' ;
shiftOp: '<<' | '>>' ;
relationalOp: '<' | '>' | '<=' | '>=' ;
equalityOp: '==' | '!=' ;
bitwiseAndOp: '&' ;
bitwiseXorOp: '^' ;
bitwiseOrOp: '|' ;
rangeOp: 'to' | 'downto' ;
postfixOperator: '++' | '--' ;
prefixOperator: '+' | '-' | '~' ;
```
### B. Statement Rule Grouping
**Current Problem**: 25 alternatives with no grouping, hard to extend (lines 98-124)
**Optimization**: Group by category:
```antlr
statement
: directive
| declaration
| controlFlow
| assignment
| jumpStatement
| loopStatement
| subroutine
| inlineasm
| labeldef
| alias
;
declaration
: variabledeclaration
| structdeclaration
| subroutinedeclaration
;
controlFlow
: if_stmt
| branch_stmt
| whenstmt
| ongoto
;
jumpStatement
: unconditionaljump
| returnstmt
| breakstmt
| continuestmt
;
loopStatement
: forloop
| whileloop
| untilloop
| repeatloop
| unrollloop
;
```
### C. Directive Rule Simplification
**Current Problem**: Complex, ambiguous alternatives (line 156)
**Optimization**: Simplify structure:
```antlr
directive
: '%' name=UNICODEDNAME '!'? directiveArgs
;
directiveArgs
: '(' EOL? scoped_identifier (',' EOL? scoped_identifier)* ','? EOL? ')' // List
| directiveArg (',' directiveArg)* // Regular args
| // Empty
;
```
---
## 3. Lexer Improvements
### A. NOT_IN Token Fix
**Current Problem**: Fragile lexer rule with whitespace dependence (line 72)
```antlr
NOT_IN: 'not' [ \t]+ 'in' [ \t] ;
```
**Fix**: Move to parser rule:
```antlr
// Remove NOT_IN from lexer
// In parser:
expression
: ...
| left=expression 'not' 'in' right=expression #NotInExpression
;
```
### B. Identifier Rule Improvement
**Current Problem**: Must manually list keywords that can be identifiers (line 266)
```antlr
identifier: UNICODEDNAME | UNDERSCORENAME | ON | CALL | INLINE | STEP ;
```
**Fix**: Use parser rule approach:
```antlr
// Instead of tokens, use a parser rule that matches any keyword as identifier
identifier
: UNICODEDNAME
| UNDERSCORENAME
| keywordAsIdentifier
;
keywordAsIdentifier
: 'on' | 'call' | 'inline' | 'step' | 'else' | 'then' | 'goto' | 'void' | 'struct'
;
```
### C. Float Number Rules Simplification
**Current Problem**: Complex, potentially ambiguous rules (lines 51-54)
```antlr
FLOAT_NUMBER : FNUMBER (('E'|'e') ('+' | '-')? DEC_INTEGER)? ;
FNUMBER : FDOTNUMBER | FNUMDOTNUMBER ;
FDOTNUMBER : '.' (DEC_DIGIT | '_')+ ;
FNUMDOTNUMBER : DEC_DIGIT (DEC_DIGIT | '_')* FDOTNUMBER? ;
```
**Fix**: Simplify:
```antlr
FLOAT_NUMBER
: DEC_DIGIT (DEC_DIGIT | '_')* ('.' (DEC_DIGIT | '_')*)?
(('E'|'e') ('+'|'-')? DEC_INTEGER)?
| '.' (DEC_DIGIT | '_')+ (('E'|'e') ('+'|'-')? DEC_INTEGER)?
;
```
---
## 4. Specific Problem Areas
### A. Pointer Dereference Cleanup
**Current Problem**: Described as "cursed mix" in comment, doesn't handle chaining well (lines 343-352)
**Current Grammar**:
```antlr
pointerdereference: (prefix=scoped_identifier '.')? derefchain ('.' field=identifier)? ;
derefchain: singlederef ('.' singlederef)* ;
singlederef: identifier arrayindex? POINTER ;
```
**Improved Structure**:
```antlr
pointerdereference
: pointerBase ('.' pointerElement)*
;
pointerBase
: scoped_identifier '.'? // Optional base with dot
| // Or start with dereference
;
pointerElement
: identifier arrayindex? POINTER // array^^ or just pointer^^
| identifier // field access
;
```
### B. Assignment Rule Simplification
**Current Problem**: Multiple ambiguous alternatives (line 182)
```antlr
assignment
: (assign_target '=' expression)
| (assign_target '=' assignment) // Chained assignment - problematic
| (multi_assign_target '=' expression)
;
```
**Fix**: Remove chained assignment ambiguity:
```antlr
assignment
: assign_target '=' expression
| multi_assign_target '=' expression
// Remove: assign_target '=' assignment // Too ambiguous
;
```
### C. Module Rule EOL Handling
**Current Problem**: Complex EOL* patterns everywhere (line 79)
```antlr
module: EOL* (module_element (EOL+ module_element)*)? EOL* EOF;
```
**Note**: This is actually necessary due to comment/EOL interleaving (issue #47), but could be cleaner with a channel-based approach.
---
## 5. Missing Syntactic Validations
Add parser-level validations to catch errors early:
```antlr
// Array type validation
vardecl
: datatype (arrayindex | EMPTYARRAYSIG)? TAG* identifierlist
| datatype EMPTYARRAYSIG identifierlist
{ if (!$datatype.text.equals("ubyte") && !$datatype.text.equals("byte"))
notifyErrorListeners("Empty array syntax [] only valid for byte/ubyte types"); }
;
// Function call validation
functioncall_stmt
: VOID? scoped_identifier '(' EOL? expression_list? EOL? ')'
| VOID? scoped_identifier '(' EOL? expression_list? EOL? ')' '=' expression // ERROR: assignment in function call
{ notifyErrorListeners("Cannot assign to function call. Use separate statement."); }
;
```
---
## 6. Implementation Priority
| Priority | Issue | Impact | Effort | Files to Modify |
|----------|-------|--------|--------|------------------|
| **High** | Expression rule refactoring | Maintainability, error quality | Medium | `Prog8ANTLR.g4` |
| **High** | Add error recovery alternatives | User experience | Low | `Prog8ANTLR.g4` |
| **Medium** | NOT_IN to parser rule | Robustness | Low | `Prog8ANTLR.g4` |
| **Medium** | Statement rule grouping | Maintainability | Low | `Prog8ANTLR.g4` |
| **Medium** | Identifier keyword handling | Completeness | Low | `Prog8ANTLR.g4` |
| **Low** | Pointer dereference cleanup | Technical debt | Medium | `Prog8ANTLR.g4` |
| **Low** | Float number simplification | Maintainability | Low | `Prog8ANTLR.g4` |
---
## 7. Example: Improved Error Messages
### Before (Current)
```
line 5:8 mismatched input '=' expecting {<EOF>, EOL, ';', ...}
line 10:3 mismatched input 'if' expecting {'{', 'then', ...}
line 15:12 mismatched input 'on' expecting {UNICODEDNAME, ...}
```
### After (With Custom Messages)
```
line 5:8 syntax error: Cannot chain assignments. Use multiple statements instead.
line 10:3 syntax error: 'if' statement missing 'then' or '{' before condition body
line 15:12 syntax error: Variable 'on' is a keyword. Use a different name or escape it.
line 20:4 syntax error: Empty array syntax [] only valid for byte/ubyte types.
```
---
## 8. Testing Strategy
After implementing these changes:
1. **Regression Testing**: Ensure all existing test cases still pass
2. **Error Message Testing**: Create test cases for each new error message
3. **Edge Case Testing**: Test complex expressions, pointer dereferencing, etc.
4. **Performance Testing**: Verify grammar changes don't significantly impact parsing speed
---
## 9. Migration Plan
1. **Phase 1**: Implement high-priority error recovery improvements
2. **Phase 2**: Refactor expression and statement rules
3. **Phase 3**: Fix lexer issues (NOT_IN, identifier handling)
4. **Phase 4**: Clean up specific problem areas (pointers, assignments)
5. **Phase 5**: Add comprehensive error validations
Each phase should include:
- Grammar changes
- Test updates
- Documentation updates
- Performance verification
---
## 10. Related Files to Update
- `/home/irmen/Projects/prog8/parser/src/main/antlr/Prog8ANTLR.g4` - Main grammar file
- `/home/irmen/Projects/prog8/compilerAst/src/prog8/parser/Prog8Parser.kt` - Error handling
- Test files in `/home/irmen/Projects/prog8/compiler/test/` - Update for new error messages
- Documentation - Update language specification if grammar semantics change
---
## Conclusion
These improvements will significantly enhance the Prog8 parser by:
- Providing clearer, more helpful error messages
- Making the grammar more maintainable and extensible
- Fixing known parsing issues and ambiguities
- Improving overall user experience for developers using the language
The changes are designed to be backward-compatible where possible, with careful attention to maintaining existing functionality while improving the parser's robustness and usability.
---
# Appendix: BailErrorStrategy Migration Guide
## What is BailErrorStrategy?
`BailErrorStrategy` is an ANTLR4 error handling strategy that **immediately stops parsing** when it encounters the first syntax error, rather than attempting to recover and continue parsing.
### Key Characteristics:
1. **Fail-Fast**: Stops on the first error - no recovery attempts
2. **No Error Recovery**: Doesn't try to skip tokens or resynchronize
3. **Throws Exceptions**: Immediately throws `InputMismatchException` or other parse errors
4. **Simple but Limited**: Easy to implement but poor user experience
### Current Implementation in Prog8:
```kotlin
// File: /home/irmen/Projects/prog8/compilerAst/src/prog8/parser/Prog8Parser.kt
private object Prog8ErrorStrategy: BailErrorStrategy() {
override fun recover(recognizer: Parser?, e: RecognitionException?) {
fillIn(e, recognizer!!.context)
reportError(recognizer, e)
}
override fun recoverInline(recognizer: Parser?): Token {
val e = InputMismatchException(recognizer)
fillIn(e, recognizer!!.context)
reportError(recognizer, e)
throw e
}
}
```
## Problems with BailErrorStrategy:
1. **Single Error Only**: Users only see the first syntax error, not all issues
2. **Poor IDE Integration**: IDEs can't highlight multiple errors simultaneously
3. **Frustrating Workflow**: Fix one error, recompile, find next error
4. **Limited Context**: No information about what might be expected
---
## Migration Plan: Replace BailErrorStrategy
### 1. **Replace Error Strategy**
**Current:**
```kotlin
parser.errorHandler = Prog8ErrorStrategy // BailErrorStrategy
```
**New:**
```kotlin
parser.errorHandler = DefaultErrorStrategy() // Built-in recovery strategy
```
### 2. **Implement Custom Error Listener**
**Create new error listener:**
```kotlin
private class Prog8ErrorListener(val src: SourceCode): BaseErrorListener() {
private val errors = mutableListOf<ParseError>()
override fun syntaxError(recognizer: Recognizer<*, *>?,
offendingSymbol: Any?,
line: Int,
charPositionInLine: Int,
msg: String,
e: RecognitionException?) {
// Collect errors instead of throwing immediately
val error = ParseError(msg, Position(src.origin, line, charPositionInLine+1, charPositionInLine+1), e ?: RuntimeException("parse error"))
errors.add(error)
}
fun getErrors(): List<ParseError> = errors.toList()
fun hasErrors(): Boolean = errors.isNotEmpty()
}
```
### 3. **Update Parser Setup**
**Current:**
```kotlin
fun parseModule(src: SourceCode): Module {
val antlrErrorListener = AntlrErrorListener(src)
val lexer = Prog8ANTLRLexer(CharStreams.fromString(src.text, src.origin))
lexer.removeErrorListeners()
lexer.addErrorListener(antlrErrorListener)
val tokens = CommonTokenStream(lexer)
val parser = Prog8ANTLRParser(tokens)
parser.errorHandler = Prog8ErrorStrategy // BailErrorStrategy
parser.removeErrorListeners()
parser.addErrorListener(antlrErrorListener)
val parseTree = parser.module()
// ... visitor pattern
}
```
**New:**
```kotlin
fun parseModule(src: SourceCode): Module {
val errorListener = Prog8ErrorListener(src)
val lexer = Prog8ANTLRLexer(CharStreams.fromString(src.text, src.origin))
lexer.removeErrorListeners()
lexer.addErrorListener(errorListener)
val tokens = CommonTokenStream(lexer)
val parser = Prog8ANTLRParser(tokens)
parser.errorHandler = DefaultErrorStrategy() // Recovery strategy
parser.removeErrorListeners()
parser.addErrorListener(errorListener)
val parseTree = parser.module()
// Check for errors after parsing
if (errorListener.hasErrors()) {
throw MultipleParseErrors(errorListener.getErrors())
}
// ... visitor pattern
}
```
### 4. **Create Multiple Errors Exception**
```kotlin
class MultipleParseErrors(val errors: List<ParseError>) : Exception() {
override val message: String
get() = "Found ${errors.size} parse errors:\n" +
errors.joinToString("\n") { "${it.position}: ${it.message}" }
}
```
### 5. **Update Compiler Error Handling**
**Current in Compiler.kt:**
```kotlin
} catch (px: ParseError) {
args.errors.printSingleError("${px.position.toClickableStr()} parse error: ${px.message}".trim())
}
```
**New:**
```kotlin
} catch (mpe: MultipleParseErrors) {
// Report all parse errors
mpe.errors.forEach { error ->
args.errors.printSingleError("${error.position.toClickableStr()} parse error: ${error.message}".trim())
}
} catch (px: ParseError) {
// Fallback for single errors
args.errors.printSingleError("${px.position.toClickableStr()} parse error: ${px.message}".trim())
}
```
---
## Grammar Changes for Better Error Recovery
### 1. **Add Error Recovery Alternatives**
```antlr
// Statement with error recovery
statement
: directive
| ongoto
| variabledeclaration
| structdeclaration
| assignment
| augassignment
| unconditionaljump
| postincrdecr
| functioncall_stmt
| if_stmt
| branch_stmt
| subroutinedeclaration
| inlineasm
| returnstmt
| forloop
| whileloop
| untilloop
| repeatloop
| unrollloop
| whenstmt
| breakstmt
| continuestmt
| labeldef
| defer
| alias
// Error recovery alternatives
| 'if' expression error=statement
{ notifyErrorListeners("Expected 'then' or '{' after if condition"); }
| 'for' identifier 'in' error=statement
{ notifyErrorListeners("Expected expression after 'in'"); }
| 'while' error=statement
{ notifyErrorListeners("Expected condition after 'while'"); }
| 'return' error=expression
{ notifyErrorListeners("Invalid return expression"); }
;
```
### 2. **Add Synchronization Points**
```antlr
// Block with synchronization
block: identifier integerliteral? EOL? '{' EOL? (block_statement | EOL)* '}'
| identifier integerliteral? EOL? '{' error=EOL? (block_statement | EOL)* '}'
{ notifyErrorListeners("Error in block: " + $error.text); }
;
// Module with synchronization
module: EOL* (module_element (EOL+ module_element)*)? EOL* EOF
| EOL* module_element error=EOL+ module_element* EOL* EOF
{ notifyErrorListeners("Error between module elements"); }
;
```
### 3. **Add Error Tokens**
```antlr
// Add to lexer
ERROR_TOKEN: . -> skip ; // Skip unknown tokens
```
---
## Benefits of Migration
### 1. **Multiple Error Reporting**
- Users see all syntax errors at once
- Better IDE integration with multiple error highlights
- More efficient development workflow
### 2. **Better Error Context**
- ANTLR's recovery provides context about expected tokens
- Can suggest alternatives based on grammar
- More precise error locations
### 3. **Improved User Experience**
- Less frustrating compilation process
- Better error messages with context
- Ability to fix multiple issues in one iteration
---
## Implementation Steps
### Phase 1: Basic Migration
1. Replace `BailErrorStrategy` with `DefaultErrorStrategy`
2. Update error listener to collect instead of throw
3. Create `MultipleParseErrors` exception
4. Update compiler error handling
### Phase 2: Grammar Improvements
1. Add error recovery alternatives to key rules
2. Add synchronization points for better recovery
3. Improve error messages with context
### Phase 3: Advanced Features
1. Add error suggestion logic
2. Implement custom recovery strategies for specific patterns
3. Add error severity levels (warning vs error)
---
## Testing Strategy
### 1. **Create Test Cases with Multiple Errors**
```kotlin
test("multiple parse errors") {
val src = """
sub main() {
x = 1 + // Missing right operand
if x > 0 // Missing then/block
y = // Missing expression
}
"""
// Should report all 3 errors, not just the first
}
```
### 2. **Test Error Recovery**
```kotlin
test("error recovery continues parsing") {
val src = """
sub bad() { x = 1 + }
sub good() { return 42 }
"""
// Should parse both subroutines and report error in first
}
```
### 3. **Test Synchronization**
```kotlin
test("block synchronization") {
val src = """
block1 {
x = 1 + // Error in block1
}
block2 { // Should still parse block2
y = 2
}
"""
// Should recover and parse block2 correctly
}
```
---
## Potential Challenges
### 1. **Cascading Errors**
- One syntax error might cause multiple subsequent errors
- Need to filter or prioritize errors intelligently
### 2. **Performance Impact**
- Error recovery has overhead
- Need to benchmark parsing performance
### 3. **False Positives**
- Recovery might parse invalid constructs
- Need to validate AST after parsing
---
## Summary
Migrating from `BailErrorStrategy` to a recovery-based approach will:
1. **Improve User Experience**: Show all errors at once instead of one-by-one
2. **Better IDE Integration**: Enable multiple error highlights
3. **Provide Context**: Better error messages with expected tokens
4. **Maintain Robustness**: Still catch all errors, just report them differently
The migration requires changes to:
- Error handling strategy in parser setup
- Error listener implementation
- Compiler error reporting
- Grammar for better recovery
- Test cases for multiple errors
This change will significantly improve the development experience for Prog8 programmers while maintaining the compiler's accuracy and robustness.
@@ -436,7 +436,7 @@ enum class Opcode {
ALIGN
}
val OpcodesThatBranchUnconditionally = arrayOf(
val OpcodesThatBranchUnconditionally = setOf(
Opcode.JUMP,
Opcode.JUMPI,
Opcode.RETURN,
@@ -444,7 +444,7 @@ val OpcodesThatBranchUnconditionally = arrayOf(
Opcode.RETURNI
)
val OpcodesThatBranch = OpcodesThatBranchUnconditionally + arrayOf(
val OpcodesThatBranch = OpcodesThatBranchUnconditionally + setOf(
Opcode.CALLI,
Opcode.CALL,
Opcode.CALLFAR,
@@ -472,7 +472,7 @@ val OpcodesThatBranch = OpcodesThatBranchUnconditionally + arrayOf(
Opcode.BLES
)
val OpcodesThatEndSSAblock = OpcodesThatBranchUnconditionally + arrayOf(
val OpcodesThatEndSSAblock = OpcodesThatBranchUnconditionally + setOf(
Opcode.BSTCC,
Opcode.BSTCS,
Opcode.BSTEQ,
@@ -1143,13 +1143,13 @@ data class IRInstruction(
if(opcode==Opcode.SQRT)
return IRDataType.WORD
}
if(opcode in arrayOf(Opcode.JUMPI, Opcode.CALLI, Opcode.STOREZI, Opcode.LSIGW, Opcode.MSIGW))
if(opcode in setOf(Opcode.JUMPI, Opcode.CALLI, Opcode.STOREZI, Opcode.LSIGW, Opcode.MSIGW))
return IRDataType.WORD
if(opcode==Opcode.EXT || opcode==Opcode.EXTS)
return if (type == IRDataType.BYTE) IRDataType.WORD else null
if(opcode==Opcode.CONCAT)
return if (type == IRDataType.BYTE) IRDataType.WORD else null
if(opcode in arrayOf(Opcode.ASRNM, Opcode.LSRNM, Opcode.LSLNM, Opcode.SQRT, Opcode.LSIGB, Opcode.MSIGB, Opcode.BSIGB))
if(opcode in setOf(Opcode.ASRNM, Opcode.LSRNM, Opcode.LSLNM, Opcode.SQRT, Opcode.LSIGB, Opcode.MSIGB, Opcode.BSIGB))
return IRDataType.BYTE
return this.type
}