mirror of
https://github.com/irmen/prog8.git
synced 2026-10-24 03:02:20 +00:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
02a1deb1e0 | ||
|
|
3ba8e5de10 | ||
|
|
819d1b4af7 | ||
|
|
cdd3d37376 | ||
|
|
d0bc01d618 | ||
|
|
ffab73334a | ||
|
|
83aeb8846c | ||
|
|
3a2087310b | ||
|
|
61b616701e | ||
|
|
bddf872906 | ||
|
|
48214a6fdb | ||
|
|
b7340856df | ||
|
|
2685854113 | ||
|
|
ad92f39469 | ||
|
|
d3fe041e01 | ||
|
|
f32bac5004 | ||
|
|
9a8e4ac105 | ||
|
|
78857e5c22 | ||
|
|
1bb828875d | ||
|
|
82aa67ffda | ||
|
|
7c44835772 | ||
|
|
8b9c63fe90 | ||
|
|
e77fcc8c16 | ||
|
|
08c12131d4 |
No files matched your search
@@ -49,6 +49,7 @@ You are writing **6502/65C02 assembly** using **64tass syntax**, in separate `*.
|
||||
- **CX16 target only**: can use 65C02 instructions — `stz`, `phx`, `plx`, `phy`, `ply`, `bra`, `trb`, `tsb`, `stp`, `wai`, `clr`, `ina`, `dea`, `cmp (zp)`, `dec`/`inc abs,x` etc.
|
||||
- **C64, C128, PET32 targets**: original 6502 only — no `stz`, no `phx`/`plx`/`phy`/`ply`, no `bra`
|
||||
- Check the target before using 65C02-specific instructions
|
||||
- **6502 / 65C02 instruction reference**: https://www.pagetable.com/c64ref/6502/?cpu=65c02&tab=2
|
||||
|
||||
## Calling Convention / Register Conventions
|
||||
- **Accumulator (A)**: 8-bit, used for most arithmetic, data movement, return values
|
||||
|
||||
@@ -76,6 +76,11 @@ No call stack for variable storage — recursion overwrites locals. To handle it
|
||||
- CPU status flag branches: `if_cs`, `if_cc`, `if_z`, `if_nz` (compile to single 6502 branch instructions)
|
||||
- Use `when` with choice blocks instead of multiple `if`
|
||||
- If-expressions for simple value assignments based on a choice
|
||||
- **Optional braces in if/else**: when the `if` or `else` body is a single statement, the `{ }` can be omitted. Place the statement on the next line, indented. Example:
|
||||
```
|
||||
if x < 5
|
||||
txt.print("small")
|
||||
```
|
||||
- `defer` defers statement execution until scope exit
|
||||
- `goto`, labels, jump lists allowed
|
||||
- **Common mistake**: `and`/`or` for bitmasking — use `&`/`|` instead!
|
||||
@@ -164,30 +169,57 @@ unroll 80 {
|
||||
- Nested subroutines access parent scope variables directly
|
||||
|
||||
## Assembly Subroutines (asmsub)
|
||||
For kernel (ROM) routines or low-level assembly that gets arguments via registers.
|
||||
- Parameter passing via registers: `@A` (accu), `@X`, `@Y`, `@AX`/`@AY` (16-bit), `@R0`-`@R15` (virtual regs), `@FAC1`/`@FAC2` (float), `@Pc` (carry), `@Pz` (zero)
|
||||
- Return value: `-> type @register` (e.g., `-> ubyte @A`, `-> bool @Pz` for immediate branch use)
|
||||
- Clobbers: `clobbers (A, X, Y)` — list all modified registers
|
||||
- Parameter names are documentation only — use registers in assembly, not parameter names
|
||||
- Create symbolic aliases at assembly start: `x1 = cx16.r0`, `x1_lo = cx16.r0L`
|
||||
- Example:
|
||||
For low-level assembly that gets arguments via registers and returns values in registers.
|
||||
- **Syntax**: `[private] [inline] asmsub subname(params) [clobbers(regs)] [-> returns] { %asm {{ ... }} }`
|
||||
- **Body Restriction**: The body of an `asmsub` **must only contain** a single `%asm {{ ... }}` node. Regular Prog8 statements or nested blocks are NOT allowed.
|
||||
- **Parameters**: `type name @register` (e.g., `ubyte val @A`, `uword addr @AX`, `float f @FAC1`).
|
||||
- **Return Values**: `-> type @register` (e.g., `-> ubyte @A`, `-> bool @Pz` for immediate branch use).
|
||||
- **Registers**:
|
||||
- **8-bit**: `@A`, `@X`, `@Y`
|
||||
- **16-bit**: `@AX`, `@AY`, `@XY` (register pairs)
|
||||
- **Float**: `@FAC1`, `@FAC2` (Floating Point Accumulators)
|
||||
- **Virtual**: `@R0`–`@R15` (16-bit), `@R0R1`–`@R14R15` (32-bit combined)
|
||||
- **Status Flags (for returns)**: `@Pc` (Carry), `@Pz` (Zero), `@Pv` (Overflow), `@Pn` (Negative)
|
||||
- **Clobbers**: `clobbers (A, X, Y)` — list all hardware registers modified by the routine.
|
||||
- **Parameter names** are for documentation and type checking only. Use the registers in your assembly code.
|
||||
- **Inlining**: `inline asmsub` will paste the assembly code directly at the call site, avoiding `jsr`/`rts` overhead.
|
||||
- **Symbol prefixes** in assembly: `p8v_` (vars), `p8s_` (subs), `p8b_` (blocks), etc.
|
||||
|
||||
## External Subroutines (extsub)
|
||||
Used to call routines at fixed memory addresses (like ROM KERNAL routines or third-party drivers).
|
||||
- **Syntax**: `[private] extsub [@bank <value>] address = subname(params) [clobbers(regs)] [-> returns]`
|
||||
- **Address**: Can be a hex literal (`$C000`) or a constant expression.
|
||||
- **Bank (optional)**: `@bank <integer>` (constant bank) or `@bank <identifier>` (variable bank).
|
||||
- **No Body**: Unlike `asmsub`, `extsub` has no `{ }` body; it just maps a signature to an address.
|
||||
- **Example**:
|
||||
```prog8
|
||||
asmsub line(uword x1 @R0, ubyte y1 @A, uword x2 @R1, ubyte y2 @Y) clobbers (A, X, Y) {
|
||||
; CX16 KERNAL CHROUT
|
||||
extsub $FFD2 = chrout(ubyte char @A) clobbers(A, X, Y)
|
||||
|
||||
; Routine in a specific RAM bank
|
||||
extsub @bank 10 $C09F = audio_init() clobbers(A, X, Y) -> bool @Pc
|
||||
```
|
||||
|
||||
## Assembly Programming Details
|
||||
- **Symbol prefixes**: `p8v_` (variables), `p8s_` (subroutines), `p8b_` (blocks), `p8c_` (constants), `p8l_` (labels), `p8t_` (structs), `p8_` (other)
|
||||
- **Fully qualified names**: `p8b_blockname.p8v_varname`, `p8b_blockname.p8s_subname.p8v_localvar`
|
||||
- **Within a `.proc`**, short names often work. `%option no_symbol_prefixing` disables prefixes (used by `cbm`, `cx16`, `txt`)
|
||||
- **Split word arrays**: append `_lsb` / `_msb` to variable name (e.g., `p8v_myarray_lsb`)
|
||||
- **CX16 target**: use 65C02 instructions (STZ, PHX, etc). Others: 6502 only
|
||||
- **Assembly syntax**: 64tass. `.proc`/`.pend`, `_label` for locals, `.byte`/`.word`/`.dword` for data, `=` for equates
|
||||
- **Instructions** like `rol`, `ror`, `asl`, `lsr` require explicit operand: `rol a`
|
||||
- **Anonymous labels**: `+` (forward), `-` (backward), branch with `+`, `++`, `+++` or `-`, `--`, `---`
|
||||
- **Register Aliases**: It's common to define aliases at the start of an `asmsub`:
|
||||
```prog8
|
||||
asmsub my_routine(uword ptr @AX) {
|
||||
%asm {{
|
||||
x1 = cx16.r0
|
||||
x2 = cx16.r1
|
||||
lda x1
|
||||
ptr_lo = p8zp_scratch_w1
|
||||
ptr_hi = p8zp_scratch_w1+1
|
||||
sta ptr_lo
|
||||
stx ptr_hi
|
||||
}}
|
||||
}
|
||||
```
|
||||
- Symbol prefixes in assembly: `p8v_` (variables), `p8s_` (subroutines), `p8b_` (blocks), `p8c_` (constants), `p8l_` (labels), `p8t_` (structs), `p8_` (other)
|
||||
- Fully qualified: `p8b_blockname.p8v_varname`, `p8b_blockname.p8s_subname.p8v_localvar`
|
||||
- Within a `.proc`, short names often work. `%option no_symbol_prefixing` disables prefixes (used by `cbm`, `cx16`, `txt`)
|
||||
- Split word arrays: append `_lsb` / `_msb` to variable name (e.g., `p8v_myarray_lsb`)
|
||||
- CX16 target only: use 65C02 instructions (STZ etc). Others: 6502 only
|
||||
- Assembly syntax: 64tass assembler. `.proc`/`.pend`, `_label` for locals, `.byte`/`.word`/`.dword` for data, `=` for equates
|
||||
- Instructions like `rol`, `ror`, `asl`, `lsr` require explicit operand: `rol a`, not just `rol`
|
||||
- Anonymous labels: `+` (forward), `-` (backward), branch with `+`, `++`, `+++` or `-`, `--`, `---`
|
||||
|
||||
## Standard Library
|
||||
- Find routines, functions, variables, modules and signatures in the symbol dump file for the given compilation target.
|
||||
|
||||
@@ -10,7 +10,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: build and install recent 64tass
|
||||
run: |
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
sudo make install
|
||||
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@v4
|
||||
uses: actions/setup-java@v5
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
@@ -32,7 +32,7 @@ jobs:
|
||||
sha256sum -b compiler/build/libs/*-all.jar > compiler/build/libs/hash.txt
|
||||
|
||||
- name: Create compiler shadowJar artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: prog8-compiler-jar-zipped
|
||||
path: |
|
||||
|
||||
Generated
+1
-1
@@ -14,6 +14,6 @@
|
||||
<option name="additionalArguments" value="-jvm-default=no-compatibility" />
|
||||
</component>
|
||||
<component name="KotlinJpsPluginSettings">
|
||||
<option name="version" value="2.3.20" />
|
||||
<option name="version" value="2.3.21" />
|
||||
</component>
|
||||
</project>
|
||||
Generated
+10
-10
@@ -1,23 +1,23 @@
|
||||
<component name="libraryTable">
|
||||
<library name="KotlinJavaRuntime" type="repository">
|
||||
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.20" />
|
||||
<properties maven-id="org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.21" />
|
||||
<CLASSES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.21/kotlin-stdlib-jdk8-2.3.21.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.21/kotlin-stdlib-2.3.21.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.21/kotlin-stdlib-jdk7-2.3.21.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.21/kotlin-stdlib-jdk8-2.3.21-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.21/kotlin-stdlib-2.3.21-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20-javadoc.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.21/kotlin-stdlib-jdk7-2.3.21-javadoc.jar!/" />
|
||||
</JAVADOC>
|
||||
<SOURCES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.21/kotlin-stdlib-jdk8-2.3.21-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.21/kotlin-stdlib-2.3.21-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20-sources.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.21/kotlin-stdlib-jdk7-2.3.21-sources.jar!/" />
|
||||
</SOURCES>
|
||||
</library>
|
||||
</component>
|
||||
Generated
+4
-4
@@ -1,14 +1,14 @@
|
||||
<component name="libraryTable">
|
||||
<library name="github.irmen.ksim65" type="repository">
|
||||
<properties maven-id="com.github.irmen:ksim65:v2.0" />
|
||||
<properties maven-id="com.github.irmen:ksim65:v2.1" />
|
||||
<CLASSES>
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/github/irmen/ksim65/v2.0/ksim65-v2.0.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/com/github/irmen/ksim65/v2.1/ksim65-v2.1.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib/2.3.20/kotlin-stdlib-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/annotations/13.0/annotations-13.0.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk8/2.3.20/kotlin-stdlib-jdk8-2.3.20.jar!/" />
|
||||
<root url="jar://$MAVEN_REPOSITORY$/org/jetbrains/kotlin/kotlin-stdlib-jdk7/2.3.20/kotlin-stdlib-jdk7-2.3.20.jar!/" />
|
||||
</CLASSES>
|
||||
<JAVADOC />
|
||||
<SOURCES />
|
||||
</library>
|
||||
</component>
|
||||
</component>
|
||||
@@ -61,8 +61,7 @@ life {
|
||||
math.rndseed(12345,9999)
|
||||
uword offset = STRIDE+1
|
||||
ubyte x
|
||||
ubyte y
|
||||
for y in 0 to HEIGHT-1 {
|
||||
repeat HEIGHT {
|
||||
for x in 0 to WIDTH-1 {
|
||||
active_world[offset+x] = math.rnd() & 1
|
||||
}
|
||||
|
||||
+2
-2
@@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
|
||||
|
||||
plugins {
|
||||
kotlin("jvm") version "2.3.20"
|
||||
kotlin("jvm") version "2.3.21"
|
||||
}
|
||||
|
||||
allprojects {
|
||||
@@ -53,7 +53,7 @@ subprojects {
|
||||
// This manages all Kotest module versions centrally
|
||||
dependencies {
|
||||
testImplementation(platform("io.kotest:kotest-bom:5.9.1"))
|
||||
// implementation("com.github.irmen:ksim65:v2.0")
|
||||
// implementation("com.github.irmen:ksim65:v2.1")
|
||||
}
|
||||
|
||||
// Common sourceSets configuration
|
||||
|
||||
@@ -173,6 +173,7 @@ val BuiltinFunctions: Map<String, FSignature> = mapOf(
|
||||
"rsave" to FSignature(false, emptyArray()),
|
||||
"rrestore" to FSignature(false, emptyArray()),
|
||||
"memory" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("name", BaseDataType.STR), FParam("size", BaseDataType.UWORD), FParam("alignment", BaseDataType.UWORD)),
|
||||
"memory__ref" to FSignature(true, arrayOf(BaseDataType.UWORD), FParam("name", BaseDataType.STR)),
|
||||
"callfar" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("arg", BaseDataType.UWORD)),
|
||||
"callfar2" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("bank", BaseDataType.UBYTE), FParam("address", BaseDataType.UWORD), FParam("argA", BaseDataType.UBYTE), FParam("argX", BaseDataType.UBYTE), FParam("argY", BaseDataType.UBYTE), FParam("argC", BaseDataType.BOOL)),
|
||||
"call" to FSignature(false, arrayOf(BaseDataType.UWORD), FParam("address", BaseDataType.UWORD)),
|
||||
|
||||
@@ -35,6 +35,11 @@ object ImportFileSystem {
|
||||
}
|
||||
}
|
||||
|
||||
fun clearCaches() {
|
||||
cache.clear()
|
||||
lineSpanCache.clear()
|
||||
}
|
||||
|
||||
fun retrieveSourceLine(position: Position): String {
|
||||
if(SourceCode.isLibraryResource(position.file)) {
|
||||
val key = SourceCode.withoutPrefix(position.file)
|
||||
|
||||
@@ -613,9 +613,12 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
lda $var1+3
|
||||
sbc $var2+3""")
|
||||
} else {
|
||||
// cmp() doesn't return a value and as such can't be used in an expression, so no need to save the temp registers' original values
|
||||
assignAsmGen.assignExpressionToRegister(arg2, RegisterOrPair.R14R15, true)
|
||||
if(!assignAsmGen.isRightTrivial(arg1))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
assignAsmGen.assignExpressionToRegister(arg1, RegisterOrPair.R12R13, true)
|
||||
if(!assignAsmGen.isRightTrivial(arg1))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda cx16.r12
|
||||
@@ -1846,7 +1849,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
if(tryOptimizedLoopCopyWithY("", "$address"))
|
||||
return emptyArray()
|
||||
|
||||
asmgen.assignVariableToRegister(address.toString(), RegisterOrPair.R14R15, null, fcall.position)
|
||||
asmgen.assignVariableToRegister(address.toString(), RegisterOrPair.R14R15, null, fcall.position, true)
|
||||
return arrayOf(RegisterOrPair.R14R15)
|
||||
}
|
||||
|
||||
@@ -1875,7 +1878,7 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
if(tryOptimizedLoopCopyWithY("lda #<$varname+$offset | sta P8ZP_SCRATCH_W1 | lda #>$varname+$offset | sta P8ZP_SCRATCH_W1+1", "(P8ZP_SCRATCH_W1)"))
|
||||
return emptyArray()
|
||||
|
||||
asmgen.assignVariableToRegister("$varname+$offset", RegisterOrPair.R14R15, null, fcall.position)
|
||||
asmgen.assignVariableToRegister("$varname+$offset", RegisterOrPair.R14R15, null, fcall.position, true)
|
||||
return arrayOf(RegisterOrPair.R14R15)
|
||||
} else if(result.second is PtIdentifier) {
|
||||
val offsetname = asmgen.asmVariableName(result.second as PtIdentifier)
|
||||
@@ -1996,7 +1999,11 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
fcall.type.isLong -> {
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R12R13, true) // left
|
||||
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R14R15, true) // right
|
||||
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.out("""
|
||||
lda cx16.r12
|
||||
cmp cx16.r14
|
||||
@@ -2095,7 +2102,11 @@ import prog8.codegen.cpu6502.assignment.*
|
||||
}
|
||||
fcall.type.isLong -> {
|
||||
asmgen.assignExpressionToRegister(fcall.args[0], RegisterOrPair.R12R13, true) // left
|
||||
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.assignExpressionToRegister(fcall.args[1], RegisterOrPair.R14R15, true) // right
|
||||
if(!assignAsmGen.isRightTrivial(fcall.args[1]))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.out("""
|
||||
lda cx16.r12
|
||||
cmp cx16.r14
|
||||
|
||||
@@ -1580,7 +1580,13 @@ internal class IfElseAsmGen(private val program: PtProgram,
|
||||
lda $opL+3
|
||||
sbc $varR+3""")
|
||||
} else {
|
||||
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
}
|
||||
asmgen.assignExpressionToRegister(r, RegisterOrPair.R14R15, true)
|
||||
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
}
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda $opL
|
||||
|
||||
@@ -656,7 +656,13 @@ internal class IfExpressionAsmGen(private val asmgen: AsmGen6502Internal, privat
|
||||
lda $opL+3
|
||||
sbc $varR+3""")
|
||||
} else {
|
||||
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
}
|
||||
asmgen.assignExpressionToRegister(r, RegisterOrPair.R14R15, true)
|
||||
if (varL == null && !assignmentAsmGen.isRightTrivial(r)) {
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
}
|
||||
asmgen.out("""
|
||||
sec
|
||||
lda $opL
|
||||
|
||||
@@ -18,6 +18,9 @@ internal class AssignmentAsmGen(
|
||||
) {
|
||||
lateinit var augmentableAsmGen: AugmentableAssignmentAsmGen
|
||||
|
||||
internal fun isRightTrivial(expr: PtExpression): Boolean =
|
||||
expr is PtNumber || expr is PtBool || expr is PtIdentifier
|
||||
|
||||
fun translate(assignment: PtAssignment) {
|
||||
val target = AsmAssignTarget.fromAstAssignment(assignment.target, assignment.definingISub(), asmgen)
|
||||
val source = AsmAssignSource.fromAstSource(assignment.value, program, asmgen).adjustSignedUnsigned(target)
|
||||
@@ -867,12 +870,8 @@ internal class AssignmentAsmGen(
|
||||
|
||||
return if(translatedOk)
|
||||
true
|
||||
else {
|
||||
if(expr.type.isLong && expr.operator in "/%")
|
||||
TODO("long division - use floats for now ${expr.position} ")
|
||||
|
||||
else
|
||||
anyExprGen.assignAnyExpressionUsingStack(expr, assign)
|
||||
}
|
||||
}
|
||||
|
||||
private fun optimizedComparison(expr: PtBinaryExpression, assign: AsmAssignment): Boolean {
|
||||
@@ -1161,6 +1160,11 @@ internal class AssignmentAsmGen(
|
||||
// assignVariableWord(target, "cx16.r15", DataType.WORD)
|
||||
// return true
|
||||
}
|
||||
expr.type.isLong -> {
|
||||
asmgen.errors.err("remainder can only be used on unsigned integer operands on 6502 target for now", expr.right.position)
|
||||
return true
|
||||
// TODO implement the signed long remainder asm routine
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
@@ -1198,6 +1202,17 @@ internal class AssignmentAsmGen(
|
||||
assignRegisterpairWord(target, RegisterOrPair.AY)
|
||||
return true
|
||||
}
|
||||
expr.type.isLong -> {
|
||||
asmgen.assignExpressionToRegister(expr.left, RegisterOrPair.R12R13, true)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.assignExpressionToRegister(expr.right, RegisterOrPair.R14R15, true)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.out(" jsr prog8_math.div_longs")
|
||||
assignRegisterLong(target, RegisterOrPair.R14R15)
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
@@ -1242,7 +1257,11 @@ internal class AssignmentAsmGen(
|
||||
}
|
||||
expr.type.isLong -> {
|
||||
asmgen.assignExpressionToRegister(expr.left, RegisterOrPair.R12R13, true)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.assignExpressionToRegister(expr.right, RegisterOrPair.R14R15, true)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
asmgen.out(" jsr prog8_math.multiply_longs")
|
||||
assignRegisterLong(target, RegisterOrPair.R14R15)
|
||||
return true
|
||||
@@ -2179,13 +2198,17 @@ internal class AssignmentAsmGen(
|
||||
else
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R12R13, 2)
|
||||
assignExpressionToRegister(expr.left, RegisterOrPair.R14R15, expr.left.type.isSigned)
|
||||
// save left operand because right eval may clobber R14R15
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
assignExpressionToRegister(expr.right, RegisterOrPair.R12R13, expr.right.type.isSigned)
|
||||
// restore left operand
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, "cx16.r12")
|
||||
assignRegisterLong(target, RegisterOrPair.R14R15)
|
||||
if(targetreg==RegisterOrPair.R14R15)
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 1)
|
||||
else
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 2)
|
||||
asmgen.popLongRegisters(RegisterOrPair.R12R13, 2)
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -2363,8 +2386,12 @@ internal class AssignmentAsmGen(
|
||||
else if(varname!=null)
|
||||
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, varname)
|
||||
else {
|
||||
// TODO: preserve R14:R15 on stack here? (the function does't do it for us)
|
||||
augmentableAsmGen.inplacemodificationLongWithExpression("cx16.r14", expr.operator, expr.right)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.pushLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
assignExpressionToRegister(expr.right, RegisterOrPair.R12R13, expr.right.type.isSigned)
|
||||
if(!isRightTrivial(expr.right))
|
||||
asmgen.popLongRegisters(RegisterOrPair.R14R15, 1)
|
||||
augmentableAsmGen.inplacemodificationLongWithVariable("cx16.r14", expr.operator, "cx16.r12")
|
||||
}
|
||||
assignRegisterLong(target, RegisterOrPair.R14R15)
|
||||
if(targetreg!=RegisterOrPair.R14R15) {
|
||||
|
||||
@@ -26,7 +26,7 @@ class IRCodeGen(
|
||||
verifyNameScoping(program, symbolTable)
|
||||
changeGlobalVarInits(symbolTable)
|
||||
|
||||
val irSymbolTable = convertStToIRSt(symbolTable)
|
||||
val irSymbolTable = convertStToIRSt(symbolTable, options.romable)
|
||||
val irProg = IRProgram(program.name, irSymbolTable, options, program.encoding)
|
||||
|
||||
// collect global variables initializers
|
||||
@@ -39,6 +39,12 @@ class IRCodeGen(
|
||||
}
|
||||
}
|
||||
|
||||
// in romable mode, generate global init code for declaration-initialized variables
|
||||
// and clear their init value so they go into the BSS section
|
||||
if(options.romable) {
|
||||
generateRomableInits(irProg)
|
||||
}
|
||||
|
||||
irProg.addAsmSymbols(options.symbolDefs)
|
||||
|
||||
for (block in program.allBlocks())
|
||||
@@ -63,6 +69,11 @@ class IRCodeGen(
|
||||
fun registerTypes(): Map<RegisterNum, IRDataType> = registers.getTypes()
|
||||
|
||||
private fun changeGlobalVarInits(symbolTable: SymbolTable) {
|
||||
// In romable mode, don't pull initializers into the symbol table;
|
||||
// they stay as assignments in the AST and become global init code naturally.
|
||||
if(options.romable)
|
||||
return
|
||||
|
||||
// Normally, block level (global) variables that have a numeric initialization value
|
||||
// are initialized via an assignment statement.
|
||||
val initsToRemove = mutableListOf<Pair<PtBlock, PtAssignment>>()
|
||||
@@ -95,6 +106,39 @@ class IRCodeGen(
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateRomableInits(irProg: IRProgram) {
|
||||
// For romable mode, declaration-initialized variables marked inBss need
|
||||
// runtime init code in the globalInits chunk, and their initializationValue
|
||||
// must be cleared so they appear in the NOINIT (BSS) section of the IR file.
|
||||
val replacements = mutableListOf<IRStStaticVariable>()
|
||||
for(variable in irProg.st.allVariables()) {
|
||||
if(variable.inBss && variable.initializationValue != null) {
|
||||
when(val initValue = variable.initializationValue) {
|
||||
is IRVariableInitializer.Numeric -> {
|
||||
val dt = irType(variable.dt)
|
||||
val tempReg = registers.next(dt)
|
||||
val chunk = IRCodeChunk(null, null)
|
||||
chunk += IRInstruction(Opcode.LOAD, dt, reg1 = tempReg, immediate = initValue.value.toInt())
|
||||
chunk += IRInstruction(Opcode.STOREM, dt, reg1 = tempReg, labelSymbol = variable.name)
|
||||
irProg.addGlobalInits(chunk)
|
||||
// replace with uninitialized version so it goes to NOINIT section
|
||||
replacements += IRStStaticVariable(
|
||||
variable.name, variable.dt, null, variable.length,
|
||||
variable.zpwish, variable.align, variable.dirty,
|
||||
variable.inBss, variable.readonly
|
||||
)
|
||||
}
|
||||
is IRVariableInitializer.Str, is IRVariableInitializer.Array -> {
|
||||
// Strings and arrays: keep inline data but now marked readonly.
|
||||
// They can serve as ROM-based read-only data.
|
||||
}
|
||||
null -> {} // uninitialized, skip
|
||||
}
|
||||
}
|
||||
}
|
||||
replacements.forEach { irProg.st.add(it) }
|
||||
}
|
||||
|
||||
private fun verifyNameScoping(program: PtProgram, symbolTable: SymbolTable) {
|
||||
fun verifyPtNode(node: PtNode) {
|
||||
when (node) {
|
||||
|
||||
@@ -3,14 +3,6 @@ package prog8.codegen.intermediate
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.intermediate.*
|
||||
|
||||
/**
|
||||
* Represents what value a register currently holds for LOADR forwarding optimization.
|
||||
*/
|
||||
private sealed class RegisterContent {
|
||||
data class Constant(val value: Any) : RegisterContent() // Int or Double
|
||||
data class Register(val originalReg: Int) : RegisterContent()
|
||||
}
|
||||
|
||||
class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA: Boolean) {
|
||||
fun optimize(optimizationsEnabled: Boolean, errors: IErrorReporter) {
|
||||
if(!optimizationsEnabled)
|
||||
@@ -65,6 +57,9 @@ class IRPeepholeOptimizer(private val irprog: IRProgram, private val retainSSA:
|
||||
|| removeNeedlessLoads(chunk1, indexedInstructions)
|
||||
|| removeDeadStores(chunk1, indexedInstructions)
|
||||
// || removeLoadrForwarding(chunk1, indexedInstructions) // DISABLED - needs debugging
|
||||
|| removeSelfIdentityOps(chunk1, indexedInstructions)
|
||||
|| simplifyShiftByZero(chunk1, indexedInstructions)
|
||||
|| cancelAdjacentOps(chunk1, indexedInstructions)
|
||||
|| removeNops(chunk1, indexedInstructions) // last time, in case one of the optimizers replaced something with a nop
|
||||
} while (changed)
|
||||
}
|
||||
@@ -544,6 +539,98 @@ jump p8_label_gen_2
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun removeSelfIdentityOps(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
var changed = false
|
||||
indexedInstructions.reversed().forEach { (idx, ins) ->
|
||||
when(ins.opcode) {
|
||||
Opcode.LOADR -> {
|
||||
if((ins.reg1 != null && ins.reg1 == ins.reg2) || (ins.fpReg1 != null && ins.fpReg1 == ins.fpReg2)) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.ANDR, Opcode.ORR -> {
|
||||
if(ins.reg1 == ins.reg2) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.XORR -> {
|
||||
if(ins.reg1 == ins.reg2) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.LOAD, ins.type, reg1 = ins.reg1, immediate = 0)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun simplifyShiftByZero(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
var changed = false
|
||||
indexedInstructions.reversed().forEach { (idx, ins) ->
|
||||
if(idx>0 && ins.opcode in setOf(Opcode.LSLN, Opcode.LSRN, Opcode.ASRN) && ins.reg2 != null) {
|
||||
val prev = indexedInstructions[idx-1].value
|
||||
if(prev.opcode == Opcode.LOAD && prev.reg1 == ins.reg2 && prev.immediate == 0) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions.removeAt(idx-1)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun cancelAdjacentOps(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
var changed = false
|
||||
indexedInstructions.reversed().forEach { (idx, ins) ->
|
||||
if(idx >= chunk.instructions.size - 1)
|
||||
return@forEach
|
||||
val insAfter = chunk.instructions[idx+1]
|
||||
|
||||
when(ins.opcode) {
|
||||
Opcode.INV -> {
|
||||
if(insAfter.opcode == Opcode.INV && insAfter.reg1 == ins.reg1) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.NEG -> {
|
||||
val sameReg = (ins.reg1 != null && ins.reg1 == insAfter.reg1) || (ins.fpReg1 != null && ins.fpReg1 == insAfter.fpReg1)
|
||||
if(insAfter.opcode == Opcode.NEG && sameReg) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.EXT, Opcode.EXTS -> {
|
||||
if(insAfter.opcode == ins.opcode && insAfter.reg1 == ins.reg1 && insAfter.type == ins.type) {
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.INC -> {
|
||||
if(insAfter.opcode == Opcode.DEC && insAfter.reg1 == ins.reg1) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
Opcode.DEC -> {
|
||||
if(insAfter.opcode == Opcode.INC && insAfter.reg1 == ins.reg1) {
|
||||
chunk.instructions[idx] = IRInstruction(Opcode.NOP)
|
||||
chunk.instructions[idx+1] = IRInstruction(Opcode.NOP)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun removeNops(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
var changed = false
|
||||
indexedInstructions.reversed().forEach { (idx, ins) ->
|
||||
@@ -686,18 +773,6 @@ jump p8_label_gen_2
|
||||
return changed
|
||||
}
|
||||
|
||||
private fun isRegisterRead(ins: IRInstruction, reg: Int): Boolean {
|
||||
// Check if the instruction reads from the given register
|
||||
val formats = instructionFormats.getValue(ins.opcode)
|
||||
val format = formats[ins.type] ?: formats[null]
|
||||
|
||||
val reg1Read = (format?.reg1 == OperandDirection.READ || format?.reg1 == OperandDirection.READWRITE) && (ins.reg1 == reg || ins.fpReg1?.value == reg)
|
||||
val reg2Read = (format?.reg2 == OperandDirection.READ || format?.reg2 == OperandDirection.READWRITE) && (ins.reg2 == reg || ins.fpReg2?.value == reg)
|
||||
val reg3Read = (format?.reg3 == OperandDirection.READ || format?.reg3 == OperandDirection.READWRITE) && (ins.reg3 == reg)
|
||||
|
||||
return reg1Read || reg2Read || reg3Read
|
||||
}
|
||||
|
||||
//private fun removeLoadrForwarding(chunk: IRCodeChunk, indexedInstructions: List<IndexedValue<IRInstruction>>): Boolean {
|
||||
// Forward LOADR instructions to their original source.
|
||||
// Example:
|
||||
|
||||
@@ -8,7 +8,7 @@ import prog8.intermediate.*
|
||||
* Converter object for transforming SymbolTable nodes to IR symbol table nodes.
|
||||
* Groups all conversion functions together for better organization.
|
||||
*/
|
||||
private object StToIrConverter {
|
||||
private class StToIrConverter(val romable: Boolean) {
|
||||
fun convert(struct: StStruct): IRStStructDef =
|
||||
IRStStructDef(struct.scopedNameString, struct.fields, struct.size)
|
||||
|
||||
@@ -72,16 +72,21 @@ private object StToIrConverter {
|
||||
}
|
||||
|
||||
val name = if('.' in variable.name) variable.name else variable.scopedNameString
|
||||
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty)
|
||||
val hasInit = initValue != null
|
||||
val inBss = romable && hasInit
|
||||
val readonly = romable && hasInit
|
||||
return IRStStaticVariable(name, variable.dt, initValue, variable.length, variable.zpwish, variable.align, variable.dirty, inBss, readonly)
|
||||
}
|
||||
|
||||
fun convert(variable: StMemVar): IRStMemVar {
|
||||
val readonly = romable && variable.address > 255u
|
||||
if('.' in variable.name) {
|
||||
return IRStMemVar(
|
||||
variable.name,
|
||||
variable.dt,
|
||||
variable.address,
|
||||
variable.length
|
||||
variable.length,
|
||||
readonly
|
||||
)
|
||||
} else {
|
||||
val scopedName = try {
|
||||
@@ -89,7 +94,7 @@ private object StToIrConverter {
|
||||
} catch (_: UninitializedPropertyAccessException) {
|
||||
variable.name
|
||||
}
|
||||
return IRStMemVar(scopedName, variable.dt, variable.address, variable.length)
|
||||
return IRStMemVar(scopedName, variable.dt, variable.address, variable.length, readonly)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,33 +139,34 @@ private object StToIrConverter {
|
||||
}
|
||||
}
|
||||
|
||||
fun convertStToIRSt(sourceSt: SymbolTable?): IRSymbolTable {
|
||||
fun convertStToIRSt(sourceSt: SymbolTable?, romable: Boolean = false): IRSymbolTable {
|
||||
val st = IRSymbolTable()
|
||||
val converter = StToIrConverter(romable)
|
||||
if (sourceSt != null) {
|
||||
sourceSt.flat.forEach {
|
||||
when(it.value.type) {
|
||||
StNodeType.STATICVAR -> st.add(StToIrConverter.convert(it.value as StStaticVariable))
|
||||
StNodeType.MEMVAR -> st.add(StToIrConverter.convert(it.value as StMemVar))
|
||||
StNodeType.STATICVAR -> st.add(converter.convert(it.value as StStaticVariable))
|
||||
StNodeType.MEMVAR -> st.add(converter.convert(it.value as StMemVar))
|
||||
StNodeType.CONSTANT -> {
|
||||
val constant = it.value as StConstant
|
||||
// If the constant has a memory() slab, add the slab to the IR symbol table first
|
||||
constant.memorySlab?.let { slab ->
|
||||
val converted = StToIrConverter.convert(slab)
|
||||
val converted = converter.convert(slab)
|
||||
st.add(converted)
|
||||
}
|
||||
st.add(StToIrConverter.convert(constant))
|
||||
st.add(converter.convert(constant))
|
||||
}
|
||||
StNodeType.MEMORYSLAB -> {
|
||||
val slab = it.value as StMemorySlab
|
||||
val converted = StToIrConverter.convert(slab)
|
||||
val converted = converter.convert(slab)
|
||||
st.add(converted)
|
||||
}
|
||||
StNodeType.STRUCTINSTANCE -> {
|
||||
val instance = it.value as StStructInstance
|
||||
val struct = sourceSt.lookup(instance.structName) as StStruct
|
||||
st.add(StToIrConverter.convert(instance, struct.fields))
|
||||
st.add(converter.convert(instance, struct.fields))
|
||||
}
|
||||
StNodeType.STRUCT -> st.add(StToIrConverter.convert(it.value as StStruct))
|
||||
StNodeType.STRUCT -> st.add(converter.convert(it.value as StStruct))
|
||||
else -> { }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.code.core.*
|
||||
import prog8.code.core.CompilationOptions
|
||||
import prog8.code.core.OutputType
|
||||
import prog8.code.core.Position
|
||||
import prog8.code.core.ZeropageType
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.codegen.intermediate.IRPeepholeOptimizer
|
||||
import prog8.intermediate.*
|
||||
@@ -289,4 +292,71 @@ class TestIRPeepholeOpt: FunSpec({
|
||||
modInstr[0].opcode shouldBe Opcode.MOD
|
||||
modInstr[0].immediate shouldBe 7
|
||||
}
|
||||
})
|
||||
|
||||
test("remove self identity operations") {
|
||||
// LOADR with same fp register is allowed (constructor only blocks integer reg1==reg2)
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOADR, IRDataType.FLOAT, fpReg1=RegisterNum(10), fpReg2=RegisterNum(10)),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 2
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 1
|
||||
instr[0].opcode shouldBe Opcode.LOAD
|
||||
instr[0].reg1 shouldBe 99
|
||||
instr[0].immediate shouldBe 42
|
||||
}
|
||||
|
||||
test("remove shift by zero") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=10, immediate=0),
|
||||
IRInstruction(Opcode.LSLN, IRDataType.BYTE, reg1=1, reg2=10),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=11, immediate=0),
|
||||
IRInstruction(Opcode.LSRN, IRDataType.BYTE, reg1=2, reg2=11),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=12, immediate=0),
|
||||
IRInstruction(Opcode.ASRN, IRDataType.BYTE, reg1=3, reg2=12),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 7
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 1
|
||||
instr[0].opcode shouldBe Opcode.LOAD
|
||||
instr[0].reg1 shouldBe 99
|
||||
instr[0].immediate shouldBe 42
|
||||
}
|
||||
|
||||
test("cancel adjacent ops") {
|
||||
val irProg = makeIRProgram(listOf(
|
||||
IRInstruction(Opcode.INV, IRDataType.BYTE, reg1=1),
|
||||
IRInstruction(Opcode.INV, IRDataType.BYTE, reg1=1),
|
||||
IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=2),
|
||||
IRInstruction(Opcode.NEG, IRDataType.BYTE, reg1=2),
|
||||
IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=3, reg2=33),
|
||||
IRInstruction(Opcode.EXT, IRDataType.BYTE, reg1=3, reg2=33),
|
||||
IRInstruction(Opcode.EXTS, IRDataType.WORD, reg1=4, reg2=44),
|
||||
IRInstruction(Opcode.EXTS, IRDataType.WORD, reg1=4, reg2=44),
|
||||
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=5),
|
||||
IRInstruction(Opcode.DEC, IRDataType.BYTE, reg1=5),
|
||||
IRInstruction(Opcode.DEC, IRDataType.BYTE, reg1=6),
|
||||
IRInstruction(Opcode.INC, IRDataType.BYTE, reg1=6),
|
||||
IRInstruction(Opcode.LOAD, IRDataType.BYTE, reg1=99, immediate=42) // keep chunk non-empty
|
||||
))
|
||||
irProg.chunks().single().instructions.size shouldBe 13
|
||||
val opt = IRPeepholeOptimizer(irProg, false)
|
||||
opt.optimize(true, ErrorReporterForTests())
|
||||
val instr = irProg.chunks().single().instructions
|
||||
instr.size shouldBe 3
|
||||
instr[0].opcode shouldBe Opcode.EXT
|
||||
instr[0].reg1 shouldBe 3
|
||||
instr[0].reg2 shouldBe 33
|
||||
instr[1].opcode shouldBe Opcode.EXTS
|
||||
instr[1].reg1 shouldBe 4
|
||||
instr[1].reg2 shouldBe 44
|
||||
instr[2].opcode shouldBe Opcode.LOAD
|
||||
instr[2].immediate shouldBe 42
|
||||
}
|
||||
})
|
||||
@@ -32,6 +32,12 @@ class VarConstantValueTypeAdjuster(
|
||||
|
||||
private fun isConstantInitializer(expr: Expression): Boolean = getConstantInitializer(expr) != null
|
||||
|
||||
private fun isMemorySlabInit(expr: Expression): Boolean =
|
||||
expr is MemorySlabRef || (expr is IFunctionCall && expr.isMemoryCall)
|
||||
|
||||
private fun requiresZp(decl: VarDecl): Boolean =
|
||||
decl.zeropage == ZeropageWish.REQUIRE_ZEROPAGE
|
||||
|
||||
override fun after(decl: VarDecl, parent: Node): Iterable<AstModification> {
|
||||
|
||||
if(decl.parent is AnonymousScope)
|
||||
@@ -47,95 +53,95 @@ class VarConstantValueTypeAdjuster(
|
||||
}
|
||||
|
||||
// replace variables by constants, if possible
|
||||
if(options.optimize) {
|
||||
if (decl.sharedWithAsm || decl.type != VarDeclType.VAR || decl.origin != VarDeclOrigin.USERCODE
|
||||
|| !(decl.datatype.isNumericOrBool || decl.datatype.isPointer))
|
||||
return noModifications
|
||||
if (decl.value != null && !isConstantInitializer(decl.value!!))
|
||||
return noModifications
|
||||
val usages = callGraph.usages(decl)
|
||||
val (writes, reads) = usages
|
||||
.partition {
|
||||
it is InlineAssembly // can't really tell if it's written to or only read, assume the worst
|
||||
|| it.parent is AssignTarget
|
||||
|| it.parent is ForLoop
|
||||
|| (it.parent as? IFunctionCall)?.target?.nameInSource?.singleOrNull() in InplaceModifyingBuiltinFunctions
|
||||
}
|
||||
|
||||
// If the address of the variable is taken, we cannot replace it with a constant.
|
||||
if (reads.any { it.parent is AddressOf })
|
||||
return noModifications
|
||||
|
||||
var singleAssignment: Assignment? = null
|
||||
val singleWrite=writes.singleOrNull()
|
||||
if(singleWrite!=null) {
|
||||
singleAssignment = singleWrite.parent as? Assignment
|
||||
if (decl.sharedWithAsm || decl.type != VarDeclType.VAR || decl.origin != VarDeclOrigin.USERCODE
|
||||
|| !(decl.datatype.isNumericOrBool || decl.datatype.isPointer))
|
||||
return noModifications
|
||||
if (decl.value != null && !isConstantInitializer(decl.value!!))
|
||||
return noModifications
|
||||
if (decl.value != null && isMemorySlabInit(decl.value!!))
|
||||
return noModifications // a pointer to a memory slab is usually accessed often and/or in ways that benefit more from the pointer being an actual variable (preferrably in zeropage)
|
||||
val usages = callGraph.usages(decl)
|
||||
val (writes, reads) = usages
|
||||
.partition {
|
||||
it is InlineAssembly // can't really tell if it's written to or only read, assume the worst
|
||||
|| it.parent is AssignTarget
|
||||
|| it.parent is ForLoop
|
||||
|| (it.parent as? IFunctionCall)?.target?.nameInSource?.singleOrNull() in InplaceModifyingBuiltinFunctions
|
||||
}
|
||||
|
||||
// If the address of the variable is taken, we cannot replace it with a constant.
|
||||
if (reads.any { it.parent is AddressOf })
|
||||
return noModifications
|
||||
|
||||
var singleAssignment: Assignment? = null
|
||||
val singleWrite=writes.singleOrNull()
|
||||
if(singleWrite!=null) {
|
||||
singleAssignment = singleWrite.parent as? Assignment
|
||||
if(singleAssignment==null) {
|
||||
singleAssignment = singleWrite.parent.parent as? Assignment
|
||||
if(singleAssignment==null) {
|
||||
singleAssignment = singleWrite.parent.parent as? Assignment
|
||||
if(singleAssignment==null) {
|
||||
// we could be part of a multi-assign
|
||||
if(singleWrite.parent is AssignTarget && singleWrite.parent.parent is AssignTarget)
|
||||
singleAssignment = singleWrite.parent.parent.parent as? Assignment
|
||||
}
|
||||
// we could be part of a multi-assign
|
||||
if(singleWrite.parent is AssignTarget && singleWrite.parent.parent is AssignTarget)
|
||||
singleAssignment = singleWrite.parent.parent.parent as? Assignment
|
||||
}
|
||||
}
|
||||
|
||||
if (singleAssignment == null) {
|
||||
if (writes.isEmpty()) {
|
||||
if(reads.isEmpty()) {
|
||||
if(decl.names.size>1) {
|
||||
errors.info("unused variable '${decl.name}'", decl.position)
|
||||
} else {
|
||||
// variable is never used AT ALL so we just remove it altogether
|
||||
if ("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(AstRemove(decl, parent as IStatementContainer))
|
||||
}
|
||||
}
|
||||
val declValue = if (decl.value != null) getConstantInitializer(decl.value!!) else null
|
||||
if (declValue != null && canBeMadeConst(decl, usages)) {
|
||||
// variable is never written to, so it can be replaced with a constant, IF the value is a constant
|
||||
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
|
||||
val const = VarDecl.builder(decl.datatype, decl.position)
|
||||
.copyFrom(decl)
|
||||
.type(VarDeclType.CONST)
|
||||
.value(declValue)
|
||||
.build()
|
||||
decl.value = null
|
||||
return listOf(
|
||||
AstReplaceNode(decl, const, parent)
|
||||
)
|
||||
}
|
||||
|
||||
if (singleAssignment == null) {
|
||||
if (writes.isEmpty()) {
|
||||
if(reads.isEmpty()) {
|
||||
if(decl.names.size>1) {
|
||||
errors.info("unused variable '${decl.name}'", decl.position)
|
||||
} else {
|
||||
// variable is never used AT ALL so we just remove it altogether
|
||||
if ("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(AstRemove(decl, parent as IStatementContainer))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (singleAssignment.origin == AssignmentOrigin.VARINIT && isConstantInitializer(singleAssignment.value) && canBeMadeConst(decl, usages)) {
|
||||
if(reads.isEmpty()) {
|
||||
if(decl.names.size>1) {
|
||||
errors.info("unused variable '${decl.name}'", decl.position)
|
||||
} else {
|
||||
// variable is never used AT ALL so we just remove it altogether, including the single assignment
|
||||
if("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(
|
||||
AstRemove(decl, parent as IStatementContainer),
|
||||
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// variable only has a single write, and it is the initialization value, so it can be replaced with a constant, but only IF the value is a constant
|
||||
val declValue = if (decl.value != null) getConstantInitializer(decl.value!!) else null
|
||||
if (declValue != null && !requiresZp(decl) && canBeMadeConst(decl, usages)) {
|
||||
// variable is never written to, so it can be replaced with a constant, IF the value is a constant
|
||||
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
|
||||
val const = VarDecl.builder(decl.datatype, decl.position)
|
||||
.copyFrom(decl)
|
||||
.type(VarDeclType.CONST)
|
||||
.value(singleAssignment.value)
|
||||
.value(declValue)
|
||||
.build()
|
||||
decl.value = null
|
||||
return listOf(
|
||||
AstReplaceNode(decl, const, parent),
|
||||
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
|
||||
AstReplaceNode(decl, const, parent)
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (singleAssignment.origin == AssignmentOrigin.VARINIT && isConstantInitializer(singleAssignment.value) && !isMemorySlabInit(singleAssignment.value) && !requiresZp(decl) && canBeMadeConst(decl, usages)) {
|
||||
if(reads.isEmpty()) {
|
||||
if(decl.names.size>1) {
|
||||
errors.info("unused variable '${decl.name}'", decl.position)
|
||||
} else {
|
||||
// variable is never used AT ALL so we just remove it altogether, including the single assignment
|
||||
if("ignore_unused" !in decl.definingBlock.options())
|
||||
errors.info("removing unused variable '${decl.name}'", decl.position)
|
||||
return listOf(
|
||||
AstRemove(decl, parent as IStatementContainer),
|
||||
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// variable only has a single write, and it is the initialization value, so it can be replaced with a constant, but only IF the value is a constant
|
||||
errors.info("variable '${decl.name}' is never written to and was made const", decl.position)
|
||||
val const = VarDecl.builder(decl.datatype, decl.position)
|
||||
.copyFrom(decl)
|
||||
.type(VarDeclType.CONST)
|
||||
.value(singleAssignment.value)
|
||||
.build()
|
||||
return listOf(
|
||||
AstReplaceNode(decl, const, parent),
|
||||
AstRemove(singleAssignment, singleAssignment.parent as IStatementContainer)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return noModifications
|
||||
@@ -148,10 +154,11 @@ class VarConstantValueTypeAdjuster(
|
||||
val p = it.parent
|
||||
val gp = p.parent
|
||||
it is PtrDereference || it is ArrayIndexedPtrDereference
|
||||
|| p is ArrayIndexedExpression || p is DirectMemoryRead || p is DirectMemoryWrite
|
||||
|| gp is DirectMemoryRead || gp is DirectMemoryWrite
|
||||
|| p is ArrayIndexedExpression || p is DirectMemoryRead || p is DirectMemoryWrite
|
||||
|| gp is DirectMemoryRead || gp is DirectMemoryWrite
|
||||
}
|
||||
if (derefCount > 2) return false
|
||||
if (derefCount > 0 && (decl.zeropage==ZeropageWish.PREFER_ZEROPAGE || decl.zeropage==ZeropageWish.REQUIRE_ZEROPAGE)) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ private fun isEmptyReturn(stmt: Statement): Boolean = stmt is Return && stmt.va
|
||||
|
||||
class Inliner(private val program: Program, private val options: CompilationOptions): AstWalker() {
|
||||
|
||||
class DetermineInlineSubs(val program: Program): IAstVisitor {
|
||||
inner class DetermineInlineSubs(val program: Program): IAstVisitor {
|
||||
private val modifications = mutableListOf<AstModification>()
|
||||
|
||||
init {
|
||||
@@ -32,23 +32,26 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return true
|
||||
|
||||
return stmt.values.all { value ->
|
||||
when (value) {
|
||||
is NumericLiteral -> true
|
||||
is IdentifierReference -> {
|
||||
makeFullyScoped(value)
|
||||
true
|
||||
}
|
||||
is FunctionCallExpression -> {
|
||||
if (value.args.size <= 1 && value.args.all {
|
||||
it is NumericLiteral || it is IdentifierReference
|
||||
}) {
|
||||
makeFullyScoped(value)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
if (isSimpleReturnExpression(value)) {
|
||||
val walker = object : AstWalker() {
|
||||
override fun after(identifier: IdentifierReference, parent: Node): Iterable<AstModification> {
|
||||
makeFullyScoped(identifier)
|
||||
return noModifications
|
||||
}
|
||||
}
|
||||
else -> false
|
||||
value.accept(walker, stmt)
|
||||
true
|
||||
} else if (value is FunctionCallExpression) {
|
||||
if (value.args.size <= 1 && value.args.all {
|
||||
it is NumericLiteral || it is IdentifierReference
|
||||
}) {
|
||||
makeFullyScoped(value)
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -223,13 +226,12 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
private fun canInlineAtCallSiteWithReason(sub: Subroutine, fcall: IFunctionCall): Pair<Boolean, String> {
|
||||
if (!sub.inline)
|
||||
return false to "subroutine not marked as inlineable"
|
||||
|
||||
|
||||
// Check parameter argument complexity
|
||||
if (sub.parameters.isNotEmpty()) {
|
||||
// Only allow inlining for void statement calls, not expression context
|
||||
if (fcall !is FunctionCallStatement || !fcall.void)
|
||||
return false to "expression context calls with parameters not supported yet"
|
||||
|
||||
if (sub.isAsmSubroutine) {
|
||||
return false to "parameterized asmsub cannot be inlined"
|
||||
}
|
||||
// Only inline if all arguments are simple (literals or identifiers)
|
||||
if (!fcall.args.all { it is NumericLiteral || it is IdentifierReference }) {
|
||||
val complexArgs = fcall.args.filterNot { it is NumericLiteral || it is IdentifierReference }
|
||||
@@ -279,13 +281,12 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
|
||||
fun inlineFunctionBody(toInline: Return): Iterable<AstModification> {
|
||||
// call site is an expression, so we have to have a Return here in the inlined sub to provide the values
|
||||
// note that we don't have to process any args, because we are currently only inlining parameterless subroutines.
|
||||
return if(toInline.values.size==1 && functionCallExpr!==toInline.values[0]) {
|
||||
return if (toInline.values.size == 1 && functionCallExpr !== toInline.values[0]) {
|
||||
// println(">>> INLINER: INLINED expression '${sub.name}' at ${functionCallExpr.position} (return value substituted)")
|
||||
sub.hasBeenInlined =true
|
||||
listOf(AstReplaceNode(functionCallExpr, toInline.values[0].copy(), parent))
|
||||
}
|
||||
else
|
||||
sub.hasBeenInlined = true
|
||||
val substitutedReturn = substituteParameters(sub, functionCallExpr, toInline.values[0]) as Expression
|
||||
listOf(AstReplaceNode(functionCallExpr, substitutedReturn, parent))
|
||||
} else
|
||||
noModifications
|
||||
}
|
||||
|
||||
@@ -303,7 +304,9 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
private fun possiblyInlineFunctioncallStmt(sub: Subroutine, origNode: Node, parent: Node): Iterable<AstModification> {
|
||||
|
||||
fun possiblyShortCircuitFunctionCall(toInline: Return): Iterable<AstModification> {
|
||||
val functionCalls = toInline.values.filterIsInstance<FunctionCallExpression>()
|
||||
// Substitute parameters in the return values first
|
||||
val substitutedReturn = substituteParameters(sub, origNode as IFunctionCall, toInline) as Return
|
||||
val functionCalls = substitutedReturn.values.filterIsInstance<FunctionCallExpression>()
|
||||
|
||||
if (functionCalls.isEmpty()) {
|
||||
// No function calls in the return values - the void call has no side effects and can be removed.
|
||||
@@ -331,15 +334,10 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
}
|
||||
|
||||
fun possiblyInlineFunctionBody(toInline: Statement): Iterable<AstModification> {
|
||||
// For void calls with parameters, we already verified in canInlineAtCallSiteWithReason()
|
||||
// that the parameter is unused in the body (so no substitution needed).
|
||||
// For expression context calls with parameters, those are rejected in canInlineAtCallSiteWithReason()
|
||||
// so we never reach here.
|
||||
// Just copy the statement - parameters are unused so no substitution needed.
|
||||
val inlinedStatement = toInline.copy()
|
||||
|
||||
val inlinedStatement = substituteParameters(sub, origNode as IFunctionCall, toInline) as Statement
|
||||
|
||||
// println(">>> INLINER: INLINED '${sub.name}' at ${functionCallStatement.position} (body inserted, ${sub.parameters.size} param(s))")
|
||||
return if(origNode !== toInline) {
|
||||
return if (origNode !== toInline) {
|
||||
sub.hasBeenInlined = true
|
||||
listOf(AstReplaceNode(origNode, inlinedStatement, parent))
|
||||
} else
|
||||
@@ -361,7 +359,6 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
sub.hasBeenInlined=true
|
||||
listOf(AstReplaceNode(origNode, bodyStmt.copy(), parent))
|
||||
} else {
|
||||
// note that we don't have to process any args, because we only inline parameterless subroutines.
|
||||
when (val toInline = bodyStmt) {
|
||||
is Return -> possiblyShortCircuitFunctionCall(toInline)
|
||||
else -> possiblyInlineFunctionBody(toInline)
|
||||
@@ -371,6 +368,19 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
return noModifications
|
||||
}
|
||||
|
||||
|
||||
private fun isSimpleReturnExpression(expr: Expression): Boolean {
|
||||
return when (expr) {
|
||||
is NumericLiteral -> true
|
||||
is IdentifierReference -> true
|
||||
is PrefixExpression -> isSimpleReturnExpression(expr.expression)
|
||||
is BinaryExpression ->
|
||||
(expr.left is NumericLiteral || expr.left is IdentifierReference) &&
|
||||
(expr.right is NumericLiteral || expr.right is IdentifierReference)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Substitutes parameter references in a statement with argument values.
|
||||
*/
|
||||
@@ -382,22 +392,25 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
val sub = fcall.target.targetStatement(program.builtinFunctions) as? Subroutine
|
||||
?: return noModifications
|
||||
|
||||
// Only handle parameterless subroutines with simple returns
|
||||
if (!sub.inline || sub.parameters.isNotEmpty() || !canInlineAtCallSite(sub, fcall))
|
||||
// Only handle inlining if sub is marked as such and call site is okay
|
||||
if (!sub.inline || !canInlineAtCallSite(sub, fcall))
|
||||
return noModifications
|
||||
|
||||
val toInline = sub.statements.firstOrNull() as? Return ?: return noModifications
|
||||
val toInline = sub.statements.firstOrNull { it !is VarDecl || it.origin != VarDeclOrigin.SUBROUTINEPARAM } as? Return ?: return noModifications
|
||||
|
||||
// Check return count matches target count
|
||||
if (toInline.values.size != multiTargets.size)
|
||||
return noModifications
|
||||
|
||||
// Only allow simple return values (literals or identifiers, no function calls)
|
||||
if (!toInline.values.all { it is NumericLiteral || it is IdentifierReference })
|
||||
// Only allow simple return values to prevent code bloat on 6502
|
||||
if (!toInline.values.all { isSimpleReturnExpression(it) })
|
||||
return noModifications
|
||||
|
||||
// Substitute parameters in all return values
|
||||
val substitutedReturn = substituteParameters(sub, fcall, toInline) as Return
|
||||
|
||||
// Create multiple single assignments, skipping void targets (they discard the value)
|
||||
val newAssignments = multiTargets.zip(toInline.values)
|
||||
val newAssignments = multiTargets.zip(substitutedReturn.values)
|
||||
.filter { (target, _) -> !target.void }
|
||||
.map { (target, value) ->
|
||||
Assignment(
|
||||
@@ -420,4 +433,59 @@ class Inliner(private val program: Program, private val options: CompilationOpti
|
||||
val scope = AnonymousScope(newAssignments.toMutableList(), assignment.position)
|
||||
return listOf(AstReplaceNode(assignment, scope, parent))
|
||||
}
|
||||
|
||||
private fun substituteParameters(sub: Subroutine, fcall: IFunctionCall, node: Node): Node {
|
||||
val paramVarDecls = sub.statements.filterIsInstance<VarDecl>().filter { it.origin == VarDeclOrigin.SUBROUTINEPARAM }
|
||||
val paramMap: Map<VarDecl, Expression> = paramVarDecls.zip(fcall.args).associate { it.first to it.second }
|
||||
|
||||
fun substitute(n: Node): Node {
|
||||
if (n is IdentifierReference) {
|
||||
val target = n.targetStatement(program.builtinFunctions)
|
||||
val arg = if (target is VarDecl) paramMap[target] else null
|
||||
if (arg != null) return arg.copy()
|
||||
}
|
||||
|
||||
return when (n) {
|
||||
is BinaryExpression -> BinaryExpression(substitute(n.left) as Expression, n.operator, substitute(n.right) as Expression, n.position)
|
||||
is PrefixExpression -> PrefixExpression(n.operator, substitute(n.expression) as Expression, n.position)
|
||||
is Return -> Return(n.values.map { substitute(it) as Expression }.toTypedArray(), n.position)
|
||||
is Assignment -> {
|
||||
val newTarget = n.target.copy()
|
||||
if (n.target.identifier != null) {
|
||||
val substituted = substitute(n.target.identifier!!)
|
||||
if (substituted is IdentifierReference) {
|
||||
newTarget.identifier = substituted
|
||||
}
|
||||
// if substituted is not an identifier, we leave the original (copied) identifier
|
||||
// in the target. This is not ideal but avoids the ClassCastException.
|
||||
}
|
||||
Assignment(
|
||||
newTarget,
|
||||
substitute(n.value) as Expression,
|
||||
n.origin,
|
||||
n.position
|
||||
)
|
||||
}
|
||||
is FunctionCallExpression -> FunctionCallExpression(
|
||||
substitute(n.target) as IdentifierReference,
|
||||
n.args.map { substitute(it) as Expression }.toMutableList(),
|
||||
n.position
|
||||
)
|
||||
is FunctionCallStatement -> FunctionCallStatement(
|
||||
substitute(n.target) as IdentifierReference,
|
||||
n.args.map { substitute(it) as Expression }.toMutableList(),
|
||||
n.void,
|
||||
n.position
|
||||
)
|
||||
is AnonymousScope -> AnonymousScope(
|
||||
n.statements.map { substitute(it) as Statement }.toMutableList(),
|
||||
n.position
|
||||
)
|
||||
is Jump -> Jump(substitute(n.target) as Expression, n.position)
|
||||
else -> n.copy()
|
||||
}
|
||||
}
|
||||
|
||||
return substitute(node)
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ dependencies {
|
||||
testImplementation(project(":intermediate"))
|
||||
testImplementation("io.kotest:kotest-runner-junit5")
|
||||
testImplementation("io.kotest:kotest-framework-datatest")
|
||||
testImplementation("com.github.irmen:ksim65:v2.0")
|
||||
testImplementation("com.github.irmen:ksim65:v2.1")
|
||||
}
|
||||
|
||||
// Exclude transitive antlr4 dependency (we only need it in parser module)
|
||||
|
||||
@@ -0,0 +1,252 @@
|
||||
; serial routines for the serial/wifi card of the X16.
|
||||
; assumes the wifi part is supported via zimodem.
|
||||
|
||||
%import strings
|
||||
|
||||
serial {
|
||||
|
||||
%option ignore_unused
|
||||
|
||||
const ubyte REG_TXRX_BUFFER = 0
|
||||
const ubyte REG_INTERRUPT_ENABLE = 1
|
||||
const ubyte REG_INTERRUPT_IDENT = 2
|
||||
const ubyte REG_FIFO_CONTROL = 2
|
||||
const ubyte REG_LINE_CONTROL = 3
|
||||
const ubyte REG_MODEM_CONTROL = 4
|
||||
const ubyte REG_LINE_STATUS = 5
|
||||
const ubyte REG_MODEM_STATUS = 6
|
||||
const ubyte REG_SCRATCH = 7
|
||||
const ubyte REG_DIVISOR_LATCH_LOW = 0 ;same as TXRX, but when bit-7 of line control high
|
||||
const ubyte REG_DIVISOR_LATCH_HI = 1 ;same as IRQE, but when bit-7 of line control high
|
||||
|
||||
; Bitfield masks for TL16C2550's Line Status Register
|
||||
enum LSR {
|
||||
DR = %00000001, ; Data Ready
|
||||
OE = %00000010, ; Overrun Error
|
||||
PE = %00000100, ; Parity Error
|
||||
FE = %00001000, ; Framing Error
|
||||
BI = %00010000, ; Break Interrupt
|
||||
THRE = %00100000, ; Transmit Holding Register Empty
|
||||
TEMT = %01000000, ; Transmitter Empty
|
||||
RCVR = %10000000, ; Error in RCVR FIFO
|
||||
}
|
||||
|
||||
; generic UART routines (not ZiModem specific):
|
||||
|
||||
sub detect_uarts() -> uword, uword {
|
||||
; Detects the presence of UART chips (up to 2) on the system.
|
||||
; Returns the discovered UART chip addresses, or 0 if none found.
|
||||
|
||||
const uword FIRST_UART_ADDRESS = $9F60
|
||||
const uword LAST_UART_ADDRESS = $9FF8
|
||||
|
||||
alias uart1 = cx16.r1
|
||||
alias uart2 = cx16.r2
|
||||
uart1 = uart2 = 0
|
||||
|
||||
for cx16.r0 in FIRST_UART_ADDRESS to LAST_UART_ADDRESS step 8 {
|
||||
if probe(cx16.r0) {
|
||||
if uart1 == 0
|
||||
uart1 = cx16.r0
|
||||
else if uart2 == 0 {
|
||||
uart2 = cx16.r0
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return uart1, uart2
|
||||
|
||||
private sub probe(uword address) -> bool {
|
||||
sys.set_irqd()
|
||||
address[REG_INTERRUPT_ENABLE] = $ff
|
||||
bool found = address[REG_INTERRUPT_ENABLE] == $0f ; The high nybble of IER is always clear
|
||||
address[REG_INTERRUPT_ENABLE] = 0
|
||||
if found {
|
||||
address[REG_MODEM_CONTROL] = $ff
|
||||
found = address[REG_MODEM_CONTROL] == $3f ; Bits 7 and 6 of MCR are always clear
|
||||
address[REG_MODEM_CONTROL] = 0
|
||||
}
|
||||
sys.clear_irqd()
|
||||
return found
|
||||
}
|
||||
}
|
||||
|
||||
sub initialize_uart(uword uart_addr) {
|
||||
; TODO baud rate is currently fixed at 921600, make this configurable
|
||||
; 921600 baud, 8,N,1, AutoFlow Control, FIFOS, no interrupts
|
||||
uart_addr[REG_INTERRUPT_ENABLE] = $00 ; No Interrupts
|
||||
uart_addr[REG_LINE_CONTROL] = $80 ; Set DLAB
|
||||
uart_addr[REG_DIVISOR_LATCH_HI] = $00
|
||||
uart_addr[REG_DIVISOR_LATCH_LOW] = $01 ; $0001 = 921600
|
||||
uart_addr[REG_LINE_CONTROL] = $03 ; 8,N,1
|
||||
uart_addr[REG_FIFO_CONTROL] = $C7 ; FIFO enable & reset
|
||||
uart_addr[REG_MODEM_CONTROL] = $23 ; DTR/RTS & AutoFlow Control
|
||||
}
|
||||
|
||||
sub write(uword @zp uart_addr, str data) {
|
||||
; Writes the string data to the uart.
|
||||
alias data_ptr = cx16.r0
|
||||
data_ptr = data
|
||||
while @(data_ptr) != 0 {
|
||||
while uart_addr[REG_LINE_STATUS] & LSR::THRE == 0 {
|
||||
; wait until data can be written
|
||||
}
|
||||
@(uart_addr) = @(data_ptr)
|
||||
data_ptr++
|
||||
}
|
||||
}
|
||||
|
||||
sub read_until(uword @zp uart_addr, str match, ^^ubyte buffer, uword max_size) -> uword {
|
||||
; Reads up to and including the match string from the uart, into buffer.
|
||||
; Stops when the match string is found or max_size bytes have been read.
|
||||
; Returns the number of bytes read.
|
||||
|
||||
alias buffer_ptr = cx16.r0
|
||||
alias match_ptr = cx16.r1
|
||||
alias data = cx16.r2L
|
||||
alias read_size = cx16.r3
|
||||
alias max_read_size = cx16.r4
|
||||
|
||||
match_ptr = match
|
||||
buffer_ptr = buffer
|
||||
max_read_size = max_size
|
||||
read_size = 0
|
||||
|
||||
while read_size < max_read_size {
|
||||
while uart_addr[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
}
|
||||
@(buffer_ptr) = data = @(uart_addr)
|
||||
buffer_ptr++
|
||||
if data == @(match_ptr) {
|
||||
match_ptr++
|
||||
if @(match_ptr)==0
|
||||
break
|
||||
}
|
||||
else
|
||||
match_ptr = match
|
||||
}
|
||||
|
||||
return read_size
|
||||
}
|
||||
|
||||
; zimodem routines:
|
||||
|
||||
sub zi_initialize(uword uart_addr) {
|
||||
; initializes the ZiModem on the given uart address
|
||||
zi_uart = uart_addr
|
||||
if zi_uart == 0
|
||||
return ; no uart present
|
||||
|
||||
initialize_uart(zi_uart)
|
||||
; ZiModem sends a version banner of the `ati` command when the ESP32 boots up.
|
||||
; Read it off if present. It is not always ready immediately so wait a tiny bit
|
||||
sys.wait(10)
|
||||
zi_write_cmd(iso:"\x03ate0") ; first send CTRL-C to abort any previous file streams
|
||||
sys.wait(5)
|
||||
if zi_uart[REG_LINE_STATUS] & LSR::DR != 0
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
zi_write_cmd("atq0v1x1f0r1s45=0s40=4096&p0b921600")
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
}
|
||||
|
||||
sub zi_reset() {
|
||||
; reset all zimodem connections
|
||||
serial.zi_write_cmd(iso:"atz")
|
||||
discard_until(zi_uart, iso:"OK\x0d\x0a")
|
||||
}
|
||||
|
||||
sub zi_write_cmd(str command) {
|
||||
; writes the zimodem command (followed by a proper line ending)
|
||||
write(zi_uart, command)
|
||||
write_eol(zi_uart)
|
||||
}
|
||||
|
||||
sub zi_start_get_file(str filename) -> long {
|
||||
; starts a file download from the given url, returns the size of the file to download.
|
||||
; you can download chunks of the file by repeatedly calling zi_get_file_chunk() until that returns 0
|
||||
write(zi_uart, iso:"at&g\"")
|
||||
write(zi_uart, filename)
|
||||
write(zi_uart, "\"")
|
||||
write_eol(zi_uart)
|
||||
|
||||
if read_one(zi_uart)=='[' {
|
||||
; parse header [ 0 <filesize> <sum> ]
|
||||
ubyte[25] header_buf
|
||||
void read_until(zi_uart, iso:"]\x0d\x0a", &header_buf, sizeof(header_buf))
|
||||
return conv.str2long(&header_buf + 3)
|
||||
} else {
|
||||
discard_until(zi_uart, iso:"RROR\x0d\x0a")
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
sub zi_get_file_chunk(^^ubyte @zp buffer, uword buffer_size, long remaining_file_size) -> uword {
|
||||
; read the next chunk of data from the file, into buffer, up to buffer_size bytes.
|
||||
; returns the number of bytes read. 0 if no more data was available.
|
||||
|
||||
uword bytes_to_read = min(remaining_file_size, buffer_size) as uword
|
||||
if bytes_to_read == 0
|
||||
return 0
|
||||
|
||||
repeat bytes_to_read {
|
||||
while zi_uart[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
}
|
||||
@(buffer) = @(zi_uart)
|
||||
buffer++
|
||||
}
|
||||
|
||||
return bytes_to_read
|
||||
}
|
||||
|
||||
sub zi_get_ip_address() -> str {
|
||||
; returns the ip address of the zimodem wifi connection
|
||||
ubyte[30] ip_buffer
|
||||
zi_write_cmd(iso:"ati2")
|
||||
void read_until(zi_uart, iso:"OK\x0d\x0a", &ip_buffer, len(ip_buffer))
|
||||
cx16.r0 = &ip_buffer
|
||||
while not strings.isspace(@(cx16.r0)) {
|
||||
cx16.r0++
|
||||
}
|
||||
@(cx16.r0) = 0
|
||||
return &ip_buffer
|
||||
}
|
||||
|
||||
|
||||
; private stuff
|
||||
|
||||
private uword @zp zi_uart ; the uart address to use for ZiModem
|
||||
|
||||
private sub discard_until(uword @zp uart_addr, str match) {
|
||||
; Reads up to and including the match string from the uart, discards all data.
|
||||
|
||||
alias match_ptr = cx16.r0
|
||||
match_ptr = match
|
||||
|
||||
repeat {
|
||||
while uart_addr[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
}
|
||||
if @(uart_addr) == @(match_ptr) {
|
||||
match_ptr++
|
||||
if @(match_ptr)==0
|
||||
break
|
||||
}
|
||||
else
|
||||
match_ptr = match
|
||||
}
|
||||
}
|
||||
|
||||
private sub read_one(uword @zp uart_addr) -> ubyte {
|
||||
while uart_addr[REG_LINE_STATUS] & LSR::DR == 0 {
|
||||
; wait until data is present
|
||||
}
|
||||
return @(uart_addr)
|
||||
}
|
||||
|
||||
private sub write_eol(uword uart_addr) {
|
||||
write(uart_addr, "\x0d\x0a")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
%import math
|
||||
|
||||
lineclip {
|
||||
|
||||
; Based on Mark Moxon's Elite deep dive: https://elite.bbcelite.com/deep_dives/line-clipping.html
|
||||
|
||||
private word cx1, cy1, cx2, cy2
|
||||
|
||||
; Set the clipping rectangle coordinates (all inclusive)
|
||||
sub set_cliprect(word x1, word y1, word x2, word y2) {
|
||||
cx1 = x1
|
||||
cy1 = y1
|
||||
cx2 = x2
|
||||
cy2 = y2
|
||||
}
|
||||
|
||||
; returns true if (x,y) is inside the clipping rectangle set by set_cliprect()
|
||||
sub inside(word x, word y) -> bool {
|
||||
return x >= cx1 and x <= cx2 and y >= cy1 and y <= cy2
|
||||
}
|
||||
|
||||
; Clip line segment (x1,y1)-(x2,y2) against the current clipping rectangle, set by set_cliprect().
|
||||
; Returns (visible, clipped_x1, clipped_y1, clipped_x2, clipped_y2).
|
||||
sub clip(word x1, word y1, word x2, word y2) -> bool, word, word, word, word {
|
||||
bool a_inside = inside(x1, y1)
|
||||
bool b_inside = inside(x2, y2)
|
||||
|
||||
if a_inside and b_inside
|
||||
return true, x1, y1, x2, y2
|
||||
|
||||
; Quick reject: both off-screen on same side
|
||||
ubyte a_code = 0
|
||||
ubyte b_code = 0
|
||||
if x1 < cx1 a_code |= 1
|
||||
if x1 > cx2 a_code |= 2
|
||||
if y1 < cy1 a_code |= 4
|
||||
if y1 > cy2 a_code |= 8
|
||||
if x2 < cx1 b_code |= 1
|
||||
if x2 > cx2 b_code |= 2
|
||||
if y2 < cy1 b_code |= 4
|
||||
if y2 > cy2 b_code |= 8
|
||||
|
||||
if (a_code & b_code) != 0
|
||||
return false, 0, 0, 0, 0
|
||||
|
||||
word ddx = x2 - x1
|
||||
word ddy = y2 - y1
|
||||
if not a_inside
|
||||
x1, y1 = clip_one(x1, y1, ddx, ddy)
|
||||
if not b_inside
|
||||
x2, y2 = clip_one(x2, y2, -ddx, -ddy)
|
||||
|
||||
if not (inside(x1, y1) and inside(x2, y2))
|
||||
return false, 0, 0, 0, 0
|
||||
|
||||
return true, x1, y1, x2, y2
|
||||
|
||||
|
||||
sub clip_one(word ax, word ay, word odx, word ody) -> word, word {
|
||||
|
||||
word x, y
|
||||
bool did_clip
|
||||
|
||||
repeat 2 {
|
||||
x = ax
|
||||
y = ay
|
||||
did_clip = false
|
||||
|
||||
if x < cx1 and odx > 0 {
|
||||
y += frac(cx1 - x, ody, odx)
|
||||
x = cx1
|
||||
did_clip = true
|
||||
} else if x > cx2 and odx < 0 {
|
||||
y += frac(cx2 - x, ody, odx)
|
||||
x = cx2
|
||||
did_clip = true
|
||||
}
|
||||
|
||||
if y < cy1 and ody > 0 {
|
||||
x += frac(cy1 - y, odx, ody)
|
||||
y = cy1
|
||||
did_clip = true
|
||||
} else if y > cy2 and ody < 0 {
|
||||
x += frac(cy2 - y, odx, ody)
|
||||
y = cy2
|
||||
did_clip = true
|
||||
}
|
||||
|
||||
ax = x
|
||||
ay = y
|
||||
if not did_clip
|
||||
break
|
||||
}
|
||||
return ax, ay
|
||||
|
||||
; (int16_t)((int32_t)dist * num / den)
|
||||
sub frac(word dist, word num, word den) -> word {
|
||||
; 16×16→32 multiply, R starts as upper word of product
|
||||
alias Q_lo = cx16.r0
|
||||
alias Q_hi = cx16.r1
|
||||
alias R = cx16.r2
|
||||
alias aden = cx16.r3
|
||||
|
||||
; No long division available on 6502, so we use restoring division 32/16->16
|
||||
aden = abs(den) as uword
|
||||
Q_hi = abs(dist) * abs(num)
|
||||
R = math.mul16_last_upper()
|
||||
Q_lo = 0
|
||||
repeat 16 {
|
||||
R = R << 1
|
||||
if (Q_hi & $8000) != 0
|
||||
R |= 1
|
||||
Q_hi <<= 1
|
||||
if (Q_lo & $8000) != 0
|
||||
Q_hi |= 1
|
||||
Q_lo <<= 1
|
||||
if R >= aden {
|
||||
R -= aden
|
||||
Q_lo |= 1
|
||||
}
|
||||
}
|
||||
|
||||
if (dist < 0) xor (num < 0) xor (den < 0)
|
||||
return -(Q_lo as word)
|
||||
return Q_lo as word
|
||||
|
||||
/*
|
||||
here is the implementation using a 32/32 long division (shorter but less efficient than 32/16):
|
||||
|
||||
uword adist = abs(dist) as uword
|
||||
uword anum = abs(num) as uword
|
||||
uword aden = abs(den) as uword
|
||||
|
||||
; do 16*16->32 bit multiplication instead of slower full 32*32
|
||||
cx16.r2 = adist * anum
|
||||
long product32 = mklong2(math.mul16_last_upper(), cx16.r2)
|
||||
|
||||
; unfortunately the long division is still needed in prog8
|
||||
uword result_u = (product32 / (aden as long)) as uword
|
||||
word result = result_u as word
|
||||
if (dist < 0) xor (num < 0) xor (den < 0)
|
||||
result = -result
|
||||
return result
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1216,3 +1216,173 @@ _nosqadd:
|
||||
rts
|
||||
|
||||
.pend
|
||||
|
||||
|
||||
|
||||
|
||||
div_longs .proc
|
||||
; 32-bit SIGNED division.
|
||||
; long1 in cx16.r12/r13, long2 in cx16.r14/r15, result in cx16.r14/r15
|
||||
; clobbers A,X, cx16.r0, cx16.r1, cx16.r2, P8ZP_SCRATCH_W1, P8ZP_SCRATCH_W2
|
||||
|
||||
dividend_lo = cx16.r12 ; low word of dividend (also receives quotient bits)
|
||||
dividend_hi = cx16.r13 ; high word of dividend
|
||||
divisor_lo = cx16.r14 ; low word of divisor (also output quotient)
|
||||
divisor_hi = cx16.r15 ; high word of divisor
|
||||
saved_div_lo = cx16.r0 ; absolute divisor saved copy (low word)
|
||||
saved_div_hi = cx16.r1 ; absolute divisor saved copy (high word)
|
||||
remainder_lo = P8ZP_SCRATCH_W1 ; remainder low word (zeropage)
|
||||
remainder_hi = P8ZP_SCRATCH_W2 ; remainder high word (zeropage)
|
||||
sign = P8ZP_SCRATCH_B1 ; result sign
|
||||
temp = P8ZP_SCRATCH_REG ; temporary byte storage
|
||||
|
||||
; Check for division by zero
|
||||
lda divisor_lo
|
||||
ora divisor_lo+1
|
||||
ora divisor_hi
|
||||
ora divisor_hi+1
|
||||
bne _non_zero
|
||||
lda #0
|
||||
sta divisor_lo
|
||||
sta divisor_lo+1
|
||||
sta divisor_hi
|
||||
sta divisor_hi+1
|
||||
rts
|
||||
|
||||
_non_zero
|
||||
; Determine result sign: XOR MSBs of dividend and divisor
|
||||
lda dividend_hi+1
|
||||
eor divisor_hi+1
|
||||
sta sign
|
||||
|
||||
; Take absolute value of dividend if negative
|
||||
lda dividend_hi+1
|
||||
bpl _abs_divisor
|
||||
jsr _neg_dividend
|
||||
|
||||
_abs_divisor
|
||||
; Take absolute value of divisor if negative
|
||||
lda divisor_hi+1
|
||||
bpl _save_div
|
||||
jsr _neg_divisor
|
||||
|
||||
_save_div
|
||||
; Save absolute divisor into saved_div
|
||||
lda divisor_lo
|
||||
sta saved_div_lo
|
||||
lda divisor_lo+1
|
||||
sta saved_div_lo+1
|
||||
lda divisor_hi
|
||||
sta saved_div_hi
|
||||
lda divisor_hi+1
|
||||
sta saved_div_hi+1
|
||||
|
||||
; Clear remainder
|
||||
lda #0
|
||||
sta remainder_lo
|
||||
sta remainder_lo+1
|
||||
sta remainder_hi
|
||||
sta remainder_hi+1
|
||||
|
||||
; 32-bit restoring division loop (32 iterations)
|
||||
ldx #32
|
||||
|
||||
_loop
|
||||
; Shift (remainder : dividend) left 1 bit
|
||||
asl dividend_lo
|
||||
rol dividend_lo+1
|
||||
rol dividend_hi
|
||||
rol dividend_hi+1
|
||||
rol remainder_lo
|
||||
rol remainder_lo+1
|
||||
rol remainder_hi
|
||||
rol remainder_hi+1
|
||||
|
||||
; Subtract saved_div from remainder, check for borrow
|
||||
sec
|
||||
lda remainder_lo
|
||||
sbc saved_div_lo
|
||||
sta cx16.r2L ; byte 0 result -> r2L (NOT tax! X is loop counter)
|
||||
|
||||
lda remainder_lo+1
|
||||
sbc saved_div_lo+1
|
||||
sta cx16.r2H ; byte 1 result -> r2H
|
||||
|
||||
lda remainder_hi
|
||||
sbc saved_div_hi
|
||||
sta temp ; byte 2 result -> temp
|
||||
|
||||
lda remainder_hi+1
|
||||
sbc saved_div_hi+1
|
||||
bcc _next ; borrow means divisor didn't fit
|
||||
|
||||
; Divisor fits - store subtraction result as new remainder
|
||||
sta remainder_hi+1 ; byte 3 result from A
|
||||
lda temp
|
||||
sta remainder_hi ; byte 2
|
||||
lda cx16.r2H
|
||||
sta remainder_lo+1 ; byte 1
|
||||
lda cx16.r2L
|
||||
sta remainder_lo ; byte 0
|
||||
inc dividend_lo ; set quotient bit
|
||||
|
||||
_next
|
||||
dex
|
||||
bne _loop
|
||||
|
||||
; Copy quotient from dividend (r12/r13) to divisor (r14/r15)
|
||||
lda dividend_lo
|
||||
sta divisor_lo
|
||||
lda dividend_lo+1
|
||||
sta divisor_lo+1
|
||||
lda dividend_hi
|
||||
sta divisor_hi
|
||||
lda dividend_hi+1
|
||||
sta divisor_hi+1
|
||||
|
||||
; Apply sign if needed
|
||||
lda sign
|
||||
bpl _done
|
||||
jsr _neg_result
|
||||
|
||||
_done
|
||||
rts
|
||||
|
||||
; Helper: negate dividend (R12R13) in place
|
||||
_neg_dividend
|
||||
sec
|
||||
lda #0
|
||||
sbc dividend_lo
|
||||
sta dividend_lo
|
||||
lda #0
|
||||
sbc dividend_lo+1
|
||||
sta dividend_lo+1
|
||||
lda #0
|
||||
sbc dividend_hi
|
||||
sta dividend_hi
|
||||
lda #0
|
||||
sbc dividend_hi+1
|
||||
sta dividend_hi+1
|
||||
rts
|
||||
|
||||
; Helper: negate divisor (R14R15) in place
|
||||
_neg_divisor
|
||||
sec
|
||||
lda #0
|
||||
sbc divisor_lo
|
||||
sta divisor_lo
|
||||
lda #0
|
||||
sbc divisor_lo+1
|
||||
sta divisor_lo+1
|
||||
lda #0
|
||||
sbc divisor_hi
|
||||
sta divisor_hi
|
||||
lda #0
|
||||
sbc divisor_hi+1
|
||||
sta divisor_hi+1
|
||||
rts
|
||||
|
||||
; Helper: negate result (R14R15) — same as negating divisor
|
||||
_neg_result = _neg_divisor
|
||||
|
||||
.pend
|
||||
@@ -90,9 +90,9 @@ io_error:
|
||||
if filenames_buf_size<=20
|
||||
return 0
|
||||
|
||||
uword filenames_buffer_start = filenames_buffer
|
||||
ubyte files_found = 0
|
||||
uword buf_ptr = filenames_buffer
|
||||
@(buf_ptr) = 0
|
||||
@(filenames_buffer) = 0
|
||||
cbm.SETNAM(1, "$")
|
||||
cbm.SETLFS(READ_IO_CHANNEL, drivenumber, 0)
|
||||
ubyte status = 1
|
||||
@@ -138,8 +138,12 @@ io_error:
|
||||
@(name_ptr) = 0
|
||||
|
||||
if pattern_ptr==0 or strings.pattern_match(list_filename, pattern_ptr) {
|
||||
buf_ptr += strings.copy(list_filename, buf_ptr) + 1
|
||||
filenames_buffer += strings.copy(list_filename, filenames_buffer) as uword + 1
|
||||
files_found++
|
||||
if filenames_buffer - filenames_buffer_start > filenames_buf_size-20 {
|
||||
@(filenames_buffer)=0
|
||||
goto end_listing_more
|
||||
}
|
||||
}
|
||||
|
||||
while cbm.CHRIN()!=0 {
|
||||
@@ -155,8 +159,13 @@ io_error:
|
||||
}
|
||||
|
||||
end_listing:
|
||||
@(buf_ptr) = 0
|
||||
@(filenames_buffer) = 0
|
||||
status = cbm.READST()
|
||||
sys.clear_carry()
|
||||
goto io_error
|
||||
|
||||
end_listing_more:
|
||||
sys.set_carry()
|
||||
|
||||
io_error:
|
||||
cbm.CLRCHN() ; restore default i/o devices
|
||||
|
||||
@@ -2,19 +2,22 @@ package prog8
|
||||
|
||||
import kotlinx.cli.*
|
||||
import prog8.ast.AstException
|
||||
import prog8.code.source.ImportFileSystem
|
||||
import prog8.code.source.ImportFileSystem.expandTilde
|
||||
import prog8.code.target.CompilationTargets
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.code.target.getCompilationTargetByName
|
||||
import prog8.compiler.CompilationResult
|
||||
import prog8.compiler.CompilerArguments
|
||||
import prog8.compiler.ErrorReporter
|
||||
import prog8.compiler.compileProgram
|
||||
import prog8.compiler.*
|
||||
import prog8.intermediate.IRFileReader
|
||||
import prog8.intermediate.Opcode
|
||||
import java.io.File
|
||||
import java.io.*
|
||||
import java.net.ConnectException
|
||||
import java.net.StandardProtocolFamily
|
||||
import java.net.URI
|
||||
import java.net.UnixDomainSocketAddress
|
||||
import java.nio.channels.Channels
|
||||
import java.nio.channels.SocketChannel
|
||||
import java.nio.file.*
|
||||
import java.time.LocalDateTime
|
||||
import kotlin.io.path.*
|
||||
@@ -22,6 +25,11 @@ import kotlin.system.exitProcess
|
||||
|
||||
|
||||
fun main(args: Array<String>) {
|
||||
if (args.contains("--daemon-server")) {
|
||||
val socketPath = CompilerDaemon.getDefaultSocketPath()
|
||||
CompilerDaemon(socketPath).run()
|
||||
return
|
||||
}
|
||||
|
||||
// NOTE: if your compiler/IDE complains here about "Unresolved reference: buildversion",
|
||||
// it means that you have to run the gradle task once to generate this file.
|
||||
@@ -77,6 +85,7 @@ private fun compileMain(args: Array<String>): Boolean {
|
||||
val watchMode by cli.option(ArgType.Boolean, fullName = "watch", description = "continuous compilation mode (watch for file changes)")
|
||||
val version by cli.option(ArgType.Boolean, fullName = "version", description = "print compiler version and exit")
|
||||
val compareIR by cli.option(ArgType.String, fullName = "compareir", description = "compare generated .p8ir file with existing .p8ir file")
|
||||
val daemonMode by cli.option(ArgType.Boolean, fullName = "daemon", description = "use the prog8c compilation daemon (auto-starts it if not running)")
|
||||
val moduleFiles by cli.argument(ArgType.String, fullName = "modules", description = "main module file(s) to compile").optional().multiple(999)
|
||||
|
||||
try {
|
||||
@@ -188,6 +197,7 @@ private fun compileMain(args: Array<String>): Boolean {
|
||||
val results = mutableListOf<CompilationResult>()
|
||||
while(true) {
|
||||
println("Continuous watch mode active. Modules: $moduleFiles")
|
||||
ImportFileSystem.clearCaches()
|
||||
results.clear()
|
||||
for(filepathRaw in moduleFiles) {
|
||||
val filepath = pathFrom(filepathRaw).normalize()
|
||||
@@ -223,9 +233,6 @@ private fun compileMain(args: Array<String>): Boolean {
|
||||
errors = ErrorReporter(txtcolors)
|
||||
)
|
||||
val compilationResult = compileProgram(compilerArgs)
|
||||
// Note: ImportFileSystem.clearCaches() removed - it caused race conditions with parallel tests
|
||||
// where one test's cache clearing would break another test's source line lookups.
|
||||
// The cache is bounded by unique source files and doesn't need manual clearing.
|
||||
|
||||
if(checkSource==true)
|
||||
println("No output produced.")
|
||||
@@ -272,6 +279,44 @@ private fun compileMain(args: Array<String>): Boolean {
|
||||
println("\u001b[H\u001b[2J") // clear the screen
|
||||
}
|
||||
|
||||
} else if (daemonMode == true) {
|
||||
for(filepathRaw in moduleFiles) {
|
||||
val filepath = pathFrom(filepathRaw).normalize()
|
||||
val txtcolors = if(plainText==true) ErrorReporter.PlainText else ErrorReporter.AnsiColors
|
||||
val compilerArgs = CompilerArguments(
|
||||
filepath,
|
||||
if(checkSource==true) false else dontOptimize != true,
|
||||
if(checkSource==true) false else dontWriteAssembly != true,
|
||||
warnSymbolShadowing == true,
|
||||
warnImplicitTypeCasts == true,
|
||||
quietAll == true,
|
||||
quietAll == true || quietAssembler == true,
|
||||
showTimings == true,
|
||||
asmListfile == true,
|
||||
dontIncludeSourcelines != true,
|
||||
experimentalCodegen == true,
|
||||
dumpVariables == true,
|
||||
dumpSymbols == true,
|
||||
varsHighBank,
|
||||
varsGolden == true,
|
||||
slabsHighBank,
|
||||
slabsGolden == true,
|
||||
compilationTarget!!,
|
||||
breakpointCpuInstruction,
|
||||
printAst1 == true,
|
||||
printAst2 == true,
|
||||
ignoreFootguns == true,
|
||||
profilingInstrumentation == true,
|
||||
nostdlib == true,
|
||||
processedSymbols,
|
||||
srcdirs,
|
||||
outputPath,
|
||||
errors = ErrorReporter(txtcolors)
|
||||
)
|
||||
if (!compileViaDaemon(compilerArgs))
|
||||
return false
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
if((startEmulator1==true || startEmulator2==true) && moduleFiles.size>1) {
|
||||
System.err.println("can't start emulator when multiple module files are specified")
|
||||
@@ -595,3 +640,150 @@ private fun scanLibraryFiles(dump: String?, searchPattern: String?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ---- daemon client ----
|
||||
|
||||
private fun compileViaDaemon(compilerArgs: CompilerArguments): Boolean {
|
||||
val socketPath = CompilerDaemon.getDefaultSocketPath()
|
||||
var channel = connectToDaemon(socketPath)
|
||||
|
||||
if (channel == null) {
|
||||
println("Starting prog8c daemon...")
|
||||
if (!startDaemonProcess()) {
|
||||
System.err.println("Failed to start prog8c daemon")
|
||||
return false
|
||||
}
|
||||
channel = connectToDaemon(socketPath, 4_000)
|
||||
if (channel == null) {
|
||||
System.err.println("prog8c daemon did not start in time")
|
||||
return false
|
||||
}
|
||||
println("prog8c daemon started.")
|
||||
}
|
||||
|
||||
val ok = communicateWithDaemon(channel, compilerArgs)
|
||||
try { channel.close() } catch (_: Exception) {}
|
||||
return ok
|
||||
}
|
||||
|
||||
private fun startDaemonProcess(): Boolean {
|
||||
return try {
|
||||
val javaHome = System.getProperty("java.home")
|
||||
val javaCmd = "$javaHome/bin/java"
|
||||
val classpath = System.getProperty("java.class.path")
|
||||
|
||||
// Shadow JAR (fat JAR with Main-Class manifest) takes priority
|
||||
val shadowJar = classpath.split(File.pathSeparatorChar)
|
||||
.firstOrNull { it.contains("prog8c") && it.endsWith("-all.jar") }
|
||||
|
||||
val cmd = mutableListOf<String>()
|
||||
cmd.add(javaCmd)
|
||||
if (shadowJar != null) {
|
||||
cmd.addAll(listOf("-jar", shadowJar))
|
||||
} else {
|
||||
cmd.addAll(listOf("-cp", classpath, "prog8.CompilerMainKt"))
|
||||
}
|
||||
cmd.add("--daemon-server")
|
||||
|
||||
val pb = ProcessBuilder(cmd)
|
||||
pb.inheritIO()
|
||||
pb.start()
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
System.err.println("Cannot start daemon: ${e.message}")
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToDaemon(socketPath: Path): SocketChannel? {
|
||||
return try {
|
||||
val address = UnixDomainSocketAddress.of(socketPath)
|
||||
val channel = SocketChannel.open(StandardProtocolFamily.UNIX)
|
||||
channel.connect(address)
|
||||
channel
|
||||
} catch (_: ConnectException) {
|
||||
null
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun connectToDaemon(socketPath: Path, timeoutMs: Long): SocketChannel? {
|
||||
val deadline = System.currentTimeMillis() + timeoutMs
|
||||
while (System.currentTimeMillis() < deadline) {
|
||||
connectToDaemon(socketPath)?.let { return it }
|
||||
Thread.sleep(100)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun communicateWithDaemon(channel: SocketChannel, compilerArgs: CompilerArguments): Boolean {
|
||||
try {
|
||||
val writer = BufferedWriter(OutputStreamWriter(Channels.newOutputStream(channel), Charsets.UTF_8))
|
||||
val reader = BufferedReader(InputStreamReader(Channels.newInputStream(channel), Charsets.UTF_8))
|
||||
|
||||
val request = DaemonRequest(
|
||||
version = prog8.buildversion.GIT_SHA,
|
||||
filepath = compilerArgs.filepath.toString(),
|
||||
optimize = compilerArgs.optimize,
|
||||
writeAssembly = compilerArgs.writeAssembly,
|
||||
warnSymbolShadowing = compilerArgs.warnSymbolShadowing,
|
||||
warnImplicitTypeCasts = compilerArgs.warnImplicitTypeCasts,
|
||||
quietAll = compilerArgs.quietAll,
|
||||
quietAssembler = compilerArgs.quietAssembler,
|
||||
showTimings = compilerArgs.showTimings,
|
||||
asmListfile = compilerArgs.asmListfile,
|
||||
includeSourcelines = compilerArgs.includeSourcelines,
|
||||
experimentalCodegen = compilerArgs.experimentalCodegen,
|
||||
dumpVariables = compilerArgs.dumpVariables,
|
||||
dumpSymbols = compilerArgs.dumpSymbols,
|
||||
varsHighBank = compilerArgs.varsHighBank,
|
||||
varsGolden = compilerArgs.varsGolden,
|
||||
slabsHighBank = compilerArgs.slabsHighBank,
|
||||
slabsGolden = compilerArgs.slabsGolden,
|
||||
compilationTarget = compilerArgs.compilationTarget,
|
||||
breakpointCpuInstruction = compilerArgs.breakpointCpuInstruction,
|
||||
printAst1 = compilerArgs.printAst1,
|
||||
printAst2 = compilerArgs.printAst2,
|
||||
ignoreFootguns = compilerArgs.ignoreFootguns,
|
||||
profilingInstrumentation = compilerArgs.profilingInstrumentation,
|
||||
nostdlib = compilerArgs.nostdlib,
|
||||
symbolDefs = compilerArgs.symbolDefs,
|
||||
sourceDirs = compilerArgs.sourceDirs,
|
||||
outputDir = compilerArgs.outputDir.toString()
|
||||
)
|
||||
|
||||
val requestJson = DaemonProtocol.encodeRequest(request)
|
||||
writer.write(requestJson)
|
||||
writer.newLine()
|
||||
writer.flush()
|
||||
|
||||
val responseJson = reader.readLine()
|
||||
if (responseJson == null) {
|
||||
System.err.println("daemon connection lost")
|
||||
return false
|
||||
}
|
||||
|
||||
val response = DaemonProtocol.decodeResponse(responseJson)
|
||||
|
||||
if (response.versionError != null) {
|
||||
System.err.println(response.versionError)
|
||||
return false
|
||||
}
|
||||
|
||||
System.out.print(response.output)
|
||||
System.out.flush()
|
||||
|
||||
for (error in response.errors) {
|
||||
val stream = if (error.severity == "ERROR") System.err else System.out
|
||||
stream.println("${error.file?.let { "$it:" } ?: ""}${error.line}:${error.col} ${error.severity} ${error.message}")
|
||||
}
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
|
||||
return response.ok
|
||||
} finally {
|
||||
try { channel.close() } catch (_: Exception) {}
|
||||
}
|
||||
}
|
||||
@@ -539,6 +539,8 @@ private fun processAst(program: Program, errors: IErrorReporter, compilerOptions
|
||||
exitProcess(0)
|
||||
}
|
||||
|
||||
program.checkPrivateAccess(errors)
|
||||
errors.report()
|
||||
program.checkIdentifiers(errors, compilerOptions)
|
||||
errors.report()
|
||||
program.charLiteralsToUByteLiterals(compilerOptions.compTarget, errors)
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
package prog8.compiler
|
||||
|
||||
import prog8.buildversion.GIT_SHA
|
||||
import prog8.code.core.Position
|
||||
import java.io.*
|
||||
import java.net.StandardProtocolFamily
|
||||
import java.net.UnixDomainSocketAddress
|
||||
import java.nio.channels.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.measureTime
|
||||
|
||||
|
||||
internal class CompilerDaemon(private val socketPath: Path) {
|
||||
companion object {
|
||||
private const val IDLE_TIMEOUT_MS = 5 * 60_000L
|
||||
|
||||
fun getDefaultSocketPath(): Path {
|
||||
val baseDir = System.getenv("XDG_RUNTIME_DIR")
|
||||
?.let { Path.of(it) }
|
||||
?: Path.of(System.getProperty("user.home"), ".cache")
|
||||
return baseDir.resolve("prog8c-daemon.sock")
|
||||
}
|
||||
|
||||
private fun myPid(): String? = try {
|
||||
ProcessHandle.current().pid().toString()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun run() {
|
||||
socketPath.deleteIfExists()
|
||||
socketPath.parent?.let { Files.createDirectories(it) }
|
||||
|
||||
val address = UnixDomainSocketAddress.of(socketPath)
|
||||
val server = ServerSocketChannel.open(StandardProtocolFamily.UNIX).bind(address)
|
||||
server.configureBlocking(false)
|
||||
val selector = Selector.open()
|
||||
server.register(selector, SelectionKey.OP_ACCEPT)
|
||||
|
||||
val pid = myPid()
|
||||
System.err.println(buildString {
|
||||
append("prog8c daemon")
|
||||
if (pid != null) append(" (pid $pid)")
|
||||
append(" running at ${socketPath}")
|
||||
})
|
||||
|
||||
var lastRequestTime = System.currentTimeMillis()
|
||||
|
||||
try {
|
||||
while (true) {
|
||||
val idleTime = System.currentTimeMillis() - lastRequestTime
|
||||
if (idleTime > IDLE_TIMEOUT_MS && lastRequestTime > 0) {
|
||||
// daemon idle timeout reached, shutting down
|
||||
break
|
||||
}
|
||||
val timeout = maxOf(1L, IDLE_TIMEOUT_MS - idleTime)
|
||||
if (selector.select(timeout) == 0)
|
||||
continue
|
||||
|
||||
val selected = selector.selectedKeys()
|
||||
val it = selected.iterator()
|
||||
if (it.hasNext()) {
|
||||
it.next()
|
||||
it.remove()
|
||||
val client = server.accept()
|
||||
if (client != null) {
|
||||
handleClient(client)
|
||||
client.close()
|
||||
lastRequestTime = System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
selector.close()
|
||||
server.close()
|
||||
socketPath.deleteIfExists()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleClient(clientChannel: SocketChannel) {
|
||||
try {
|
||||
val reader = BufferedReader(InputStreamReader(Channels.newInputStream(clientChannel), Charsets.UTF_8))
|
||||
val writer = BufferedWriter(OutputStreamWriter(Channels.newOutputStream(clientChannel), Charsets.UTF_8))
|
||||
|
||||
val reqJson = reader.readLine() ?: return
|
||||
val request: DaemonRequest = try {
|
||||
DaemonProtocol.decodeRequest(reqJson)
|
||||
} catch (e: Exception) {
|
||||
System.err.println("daemon: failed to decode request: ${e.message}")
|
||||
return
|
||||
}
|
||||
|
||||
// Version check
|
||||
if (request.version != GIT_SHA) {
|
||||
val resp = DaemonResponse(
|
||||
ok = false,
|
||||
versionError = "daemon version mismatch: client=${request.version.take(8)}, daemon=${GIT_SHA.take(8)}",
|
||||
errors = emptyList(),
|
||||
output = "",
|
||||
t_ms = 0,
|
||||
outputFiles = emptyList(),
|
||||
importedFiles = emptyList()
|
||||
)
|
||||
writer.write(DaemonProtocol.encodeResponse(resp))
|
||||
writer.newLine()
|
||||
writer.flush()
|
||||
return
|
||||
}
|
||||
|
||||
// Capture stdout/stderr
|
||||
val outBytes = ByteArrayOutputStream()
|
||||
val errBytes = ByteArrayOutputStream()
|
||||
val oldOut = System.out
|
||||
val oldErr = System.err
|
||||
System.setOut(PrintStream(outBytes, true, Charsets.UTF_8.name()))
|
||||
System.setErr(PrintStream(errBytes, true, Charsets.UTF_8.name()))
|
||||
|
||||
val daemonErr = DaemonErrorReporter()
|
||||
var result: CompilationResult? = null
|
||||
val t_ms = try {
|
||||
measureTime {
|
||||
val compilerArgs = request.toCompilerArguments(daemonErr)
|
||||
result = compileProgram(compilerArgs)
|
||||
}.toLong(DurationUnit.MILLISECONDS)
|
||||
} catch (e: Exception) {
|
||||
System.err.println("daemon: compilation crash: ${e.message}")
|
||||
daemonErr.err("internal daemon error: ${e.message}", Position.DUMMY)
|
||||
0L
|
||||
} finally {
|
||||
System.out.flush()
|
||||
System.err.flush()
|
||||
System.setOut(oldOut)
|
||||
System.setErr(oldErr)
|
||||
}
|
||||
|
||||
val outputFiles = if (result != null)
|
||||
listOfNotNull(
|
||||
request.outputDir.let { d ->
|
||||
val name = request.filepath.substringBeforeLast('.').substringAfterLast('/').substringAfterLast('\\')
|
||||
val dir = if (d == ".") "" else "$d/"
|
||||
listOf(
|
||||
"${dir}${name}.prg".takeIf { request.writeAssembly },
|
||||
"${dir}${name}.asm".takeIf { request.writeAssembly },
|
||||
"${dir}${name}.p8ir".takeIf { !request.writeAssembly }
|
||||
).filterNotNull()
|
||||
}
|
||||
).flatten()
|
||||
else
|
||||
emptyList()
|
||||
|
||||
val importedFiles = result?.importedFiles?.map { it.toString() } ?: emptyList()
|
||||
|
||||
val combinedOutput = outBytes.toString(Charsets.UTF_8.name()) + errBytes.toString(Charsets.UTF_8.name())
|
||||
|
||||
val resp = DaemonResponse(
|
||||
ok = result != null,
|
||||
versionError = null,
|
||||
errors = daemonErr.getMessages().map { msg ->
|
||||
DaemonError(
|
||||
severity = msg.severity,
|
||||
message = msg.message,
|
||||
file = msg.position?.file,
|
||||
line = msg.position?.line ?: 1,
|
||||
col = msg.position?.startCol ?: 1
|
||||
)
|
||||
},
|
||||
output = combinedOutput,
|
||||
t_ms = t_ms,
|
||||
outputFiles = outputFiles,
|
||||
importedFiles = importedFiles
|
||||
)
|
||||
|
||||
val respJson = DaemonProtocol.encodeResponse(resp)
|
||||
writer.write(respJson)
|
||||
writer.newLine()
|
||||
writer.flush()
|
||||
} catch (e: IOException) {
|
||||
System.err.println("daemon: client I/O error: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun DaemonRequest.toCompilerArguments(daemonErr: DaemonErrorReporter): CompilerArguments {
|
||||
return CompilerArguments(
|
||||
filepath = Path.of(filepath),
|
||||
optimize = optimize,
|
||||
writeAssembly = writeAssembly,
|
||||
warnSymbolShadowing = warnSymbolShadowing,
|
||||
warnImplicitTypeCasts = warnImplicitTypeCasts,
|
||||
quietAll = quietAll,
|
||||
quietAssembler = quietAssembler,
|
||||
showTimings = showTimings,
|
||||
asmListfile = asmListfile,
|
||||
includeSourcelines = includeSourcelines,
|
||||
experimentalCodegen = experimentalCodegen,
|
||||
dumpVariables = dumpVariables,
|
||||
dumpSymbols = dumpSymbols,
|
||||
varsHighBank = varsHighBank,
|
||||
varsGolden = varsGolden,
|
||||
slabsHighBank = slabsHighBank,
|
||||
slabsGolden = slabsGolden,
|
||||
compilationTarget = compilationTarget,
|
||||
breakpointCpuInstruction = breakpointCpuInstruction,
|
||||
printAst1 = printAst1,
|
||||
printAst2 = printAst2,
|
||||
ignoreFootguns = ignoreFootguns,
|
||||
profilingInstrumentation = profilingInstrumentation,
|
||||
nostdlib = nostdlib,
|
||||
symbolDefs = symbolDefs,
|
||||
sourceDirs = sourceDirs,
|
||||
outputDir = Path.of(outputDir),
|
||||
errors = daemonErr
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package prog8.compiler
|
||||
|
||||
import prog8.code.core.ErrorsReportedException
|
||||
import prog8.code.core.IErrorReporter
|
||||
import prog8.code.core.Position
|
||||
|
||||
|
||||
internal class DaemonErrorReporter : IErrorReporter {
|
||||
data class CompilerMessage(val severity: String, val message: String, val position: Position?)
|
||||
|
||||
private val messages = mutableListOf<CompilerMessage>()
|
||||
|
||||
override fun err(msg: String, position: Position) {
|
||||
messages.add(CompilerMessage("ERROR", msg, position))
|
||||
}
|
||||
|
||||
override fun warn(msg: String, position: Position) {
|
||||
messages.add(CompilerMessage("WARNING", msg, position))
|
||||
}
|
||||
|
||||
override fun info(msg: String, position: Position) {
|
||||
messages.add(CompilerMessage("INFO", msg, position))
|
||||
}
|
||||
|
||||
override fun undefined(symbol: List<String>, suggestImport: Boolean, position: Position) {
|
||||
if (suggestImport && symbol.size > 1)
|
||||
err("undefined symbol: ${symbol.joinToString(".")} (maybe you forgot to import a module that defines ${symbol.first()}?)", position)
|
||||
else
|
||||
err("undefined symbol: ${symbol.joinToString(".")}", position)
|
||||
}
|
||||
|
||||
override fun noErrors() = messages.none { it.severity == "ERROR" }
|
||||
|
||||
override fun report() {
|
||||
// Don't print anything, but throw so compileProgram() stops and returns null
|
||||
if (!noErrors())
|
||||
throw ErrorsReportedException("")
|
||||
}
|
||||
|
||||
override fun noErrorForLine(position: Position) =
|
||||
!messages.any { it.position?.line == position.line && it.severity != "INFO" }
|
||||
|
||||
override fun printSingleError(errormessage: String) {
|
||||
messages.add(CompilerMessage("ERROR", errormessage, null))
|
||||
}
|
||||
|
||||
fun getMessages() = messages.toList()
|
||||
}
|
||||
@@ -0,0 +1,367 @@
|
||||
package prog8.compiler
|
||||
|
||||
|
||||
internal data class DaemonRequest(
|
||||
val version: String,
|
||||
val filepath: String,
|
||||
val optimize: Boolean,
|
||||
val writeAssembly: Boolean,
|
||||
val warnSymbolShadowing: Boolean,
|
||||
val warnImplicitTypeCasts: Boolean,
|
||||
val quietAll: Boolean,
|
||||
val quietAssembler: Boolean,
|
||||
val showTimings: Boolean,
|
||||
val asmListfile: Boolean,
|
||||
val includeSourcelines: Boolean,
|
||||
val experimentalCodegen: Boolean,
|
||||
val dumpVariables: Boolean,
|
||||
val dumpSymbols: Boolean,
|
||||
val varsHighBank: Int?,
|
||||
val varsGolden: Boolean,
|
||||
val slabsHighBank: Int?,
|
||||
val slabsGolden: Boolean,
|
||||
val compilationTarget: String,
|
||||
val breakpointCpuInstruction: String?,
|
||||
val printAst1: Boolean,
|
||||
val printAst2: Boolean,
|
||||
val ignoreFootguns: Boolean,
|
||||
val profilingInstrumentation: Boolean,
|
||||
val nostdlib: Boolean,
|
||||
val symbolDefs: Map<String, String>,
|
||||
val sourceDirs: List<String>,
|
||||
val outputDir: String
|
||||
)
|
||||
|
||||
internal data class DaemonResponse(
|
||||
val ok: Boolean,
|
||||
val versionError: String?,
|
||||
val errors: List<DaemonError>,
|
||||
val output: String,
|
||||
val t_ms: Long,
|
||||
val outputFiles: List<String>,
|
||||
val importedFiles: List<String>
|
||||
)
|
||||
|
||||
internal data class DaemonError(
|
||||
val severity: String,
|
||||
val message: String,
|
||||
val file: String?,
|
||||
val line: Int,
|
||||
val col: Int
|
||||
)
|
||||
|
||||
|
||||
// Minimal JSON encoder/decoder for the daemon protocol
|
||||
internal object DaemonProtocol {
|
||||
|
||||
fun encodeRequest(req: DaemonRequest): String = buildString {
|
||||
append('{')
|
||||
append(prop("version", req.version))
|
||||
append(prop("filepath", req.filepath))
|
||||
append(prop("optimize", req.optimize))
|
||||
append(prop("writeAssembly", req.writeAssembly))
|
||||
append(prop("warnSymbolShadowing", req.warnSymbolShadowing))
|
||||
append(prop("warnImplicitTypeCasts", req.warnImplicitTypeCasts))
|
||||
append(prop("quietAll", req.quietAll))
|
||||
append(prop("quietAssembler", req.quietAssembler))
|
||||
append(prop("showTimings", req.showTimings))
|
||||
append(prop("asmListfile", req.asmListfile))
|
||||
append(prop("includeSourcelines", req.includeSourcelines))
|
||||
append(prop("experimentalCodegen", req.experimentalCodegen))
|
||||
append(prop("dumpVariables", req.dumpVariables))
|
||||
append(prop("dumpSymbols", req.dumpSymbols))
|
||||
append(prop("varsHighBank", req.varsHighBank))
|
||||
append(prop("varsGolden", req.varsGolden))
|
||||
append(prop("slabsHighBank", req.slabsHighBank))
|
||||
append(prop("slabsGolden", req.slabsGolden))
|
||||
append(prop("compilationTarget", req.compilationTarget))
|
||||
append(propOpt("breakpointCpuInstruction", req.breakpointCpuInstruction))
|
||||
append(prop("printAst1", req.printAst1))
|
||||
append(prop("printAst2", req.printAst2))
|
||||
append(prop("ignoreFootguns", req.ignoreFootguns))
|
||||
append(prop("profilingInstrumentation", req.profilingInstrumentation))
|
||||
append(prop("nostdlib", req.nostdlib))
|
||||
append(prop("symbolDefs", req.symbolDefs))
|
||||
append(prop("sourceDirs", req.sourceDirs))
|
||||
append(prop("outputDir", req.outputDir))
|
||||
append("null") // placeholder, gets overwritten by trimEnd
|
||||
setLength(length - 5)
|
||||
append('}')
|
||||
}
|
||||
|
||||
fun encodeResponse(resp: DaemonResponse): String = buildString {
|
||||
append('{')
|
||||
append(prop("ok", resp.ok))
|
||||
append(propOpt("versionError", resp.versionError))
|
||||
append("\"errors\":[")
|
||||
for ((i, e) in resp.errors.withIndex()) {
|
||||
if (i > 0) append(',')
|
||||
append('{')
|
||||
append(prop("severity", e.severity))
|
||||
append(prop("message", e.message))
|
||||
append(propOpt("file", e.file))
|
||||
append(prop("line", e.line))
|
||||
append(prop("col", e.col))
|
||||
append("null")
|
||||
setLength(length - 4)
|
||||
append('}')
|
||||
}
|
||||
append("],")
|
||||
append(prop("output", resp.output))
|
||||
append(prop("t_ms", resp.t_ms))
|
||||
append(prop("outputFiles", resp.outputFiles))
|
||||
append(prop("importedFiles", resp.importedFiles))
|
||||
append("null")
|
||||
setLength(length - 5)
|
||||
append('}')
|
||||
}
|
||||
|
||||
fun decodeResponse(json: String): DaemonResponse {
|
||||
val map = parseJsonObject(json)
|
||||
val errorsJson = map["errors"] as? List<*> ?: emptyList<Any>()
|
||||
val errors = errorsJson.map { e ->
|
||||
val em = e as Map<*, *>
|
||||
DaemonError(
|
||||
severity = em["severity"] as String,
|
||||
message = em["message"] as String,
|
||||
file = em["file"] as? String,
|
||||
line = (em["line"] as Number).toInt(),
|
||||
col = (em["col"] as Number).toInt()
|
||||
)
|
||||
}
|
||||
val outputFiles = (map["outputFiles"] as? List<*>)?.map { it as String } ?: emptyList()
|
||||
val importedFiles = (map["importedFiles"] as? List<*>)?.map { it as String } ?: emptyList()
|
||||
return DaemonResponse(
|
||||
ok = map["ok"] as Boolean,
|
||||
versionError = map["versionError"] as? String,
|
||||
errors = errors,
|
||||
output = map["output"] as String,
|
||||
t_ms = (map["t_ms"] as Number).toLong(),
|
||||
outputFiles = outputFiles,
|
||||
importedFiles = importedFiles
|
||||
)
|
||||
}
|
||||
|
||||
fun decodeRequest(json: String): DaemonRequest {
|
||||
val map = parseJsonObject(json)
|
||||
return DaemonRequest(
|
||||
version = map["version"] as String,
|
||||
filepath = map["filepath"] as String,
|
||||
optimize = map["optimize"] as Boolean,
|
||||
writeAssembly = map["writeAssembly"] as Boolean,
|
||||
warnSymbolShadowing = map["warnSymbolShadowing"] as Boolean,
|
||||
warnImplicitTypeCasts = map["warnImplicitTypeCasts"] as Boolean,
|
||||
quietAll = map["quietAll"] as Boolean,
|
||||
quietAssembler = map["quietAssembler"] as Boolean,
|
||||
showTimings = map["showTimings"] as Boolean,
|
||||
asmListfile = map["asmListfile"] as Boolean,
|
||||
includeSourcelines = map["includeSourcelines"] as Boolean,
|
||||
experimentalCodegen = map["experimentalCodegen"] as Boolean,
|
||||
dumpVariables = map["dumpVariables"] as Boolean,
|
||||
dumpSymbols = map["dumpSymbols"] as Boolean,
|
||||
varsHighBank = map["varsHighBank"] as? Int,
|
||||
varsGolden = map["varsGolden"] as Boolean,
|
||||
slabsHighBank = map["slabsHighBank"] as? Int,
|
||||
slabsGolden = map["slabsGolden"] as Boolean,
|
||||
compilationTarget = map["compilationTarget"] as String,
|
||||
breakpointCpuInstruction = map["breakpointCpuInstruction"] as? String,
|
||||
printAst1 = map["printAst1"] as Boolean,
|
||||
printAst2 = map["printAst2"] as Boolean,
|
||||
ignoreFootguns = map["ignoreFootguns"] as Boolean,
|
||||
profilingInstrumentation = map["profilingInstrumentation"] as Boolean,
|
||||
nostdlib = map["nostdlib"] as Boolean,
|
||||
symbolDefs = (map["symbolDefs"] as? Map<*, *>)?.mapKeys { it.key as String }?.mapValues { it.value as String } ?: emptyMap(),
|
||||
sourceDirs = (map["sourceDirs"] as? List<*>)?.map { it as String } ?: emptyList(),
|
||||
outputDir = map["outputDir"] as String
|
||||
)
|
||||
}
|
||||
|
||||
private fun encodeJsonString(s: String): String {
|
||||
val sb = StringBuilder(s.length + 2)
|
||||
sb.append('"')
|
||||
for (c in s) {
|
||||
when (c) {
|
||||
'"' -> sb.append("\\\"")
|
||||
'\\' -> sb.append("\\\\")
|
||||
'\n' -> sb.append("\\n")
|
||||
'\r' -> sb.append("\\r")
|
||||
'\t' -> sb.append("\\t")
|
||||
else -> sb.append(c)
|
||||
}
|
||||
}
|
||||
sb.append('"')
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun prop(name: String, value: String): String = "\"$name\":${encodeJsonString(value)},"
|
||||
private fun prop(name: String, value: Boolean): String = "\"$name\":$value,"
|
||||
private fun prop(name: String, value: Int): String = "\"$name\":$value,"
|
||||
private fun prop(name: String, value: Long): String = "\"$name\":$value,"
|
||||
private fun prop(name: String, value: Int?): String = value?.let { "\"$name\":$it," } ?: "\"$name\":null,"
|
||||
private fun prop(name: String, value: Map<String, String>): String {
|
||||
val entries = value.entries.joinToString(",") { "${encodeJsonString(it.key)}:${encodeJsonString(it.value)}" }
|
||||
return "\"$name\":{$entries},"
|
||||
}
|
||||
private fun prop(name: String, value: List<String>): String {
|
||||
val items = value.joinToString(",") { encodeJsonString(it) }
|
||||
return "\"$name\":[$items],"
|
||||
}
|
||||
private fun propOpt(name: String, value: String?): String = value?.let { "\"$name\":${encodeJsonString(it)}," } ?: ""
|
||||
|
||||
private fun parseJsonObject(json: String): Map<String, Any?> {
|
||||
val trimmed = json.trim()
|
||||
if (!trimmed.startsWith('{') || !trimmed.endsWith('}'))
|
||||
throw IllegalArgumentException("not a JSON object: $json")
|
||||
val inner = trimmed.substring(1, trimmed.length - 1)
|
||||
return parsePairs(inner)
|
||||
}
|
||||
|
||||
private fun parsePairs(s: String): Map<String, Any?> {
|
||||
val result = LinkedHashMap<String, Any?>()
|
||||
var pos = 0
|
||||
while (pos < s.length) {
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
if (pos >= s.length) break
|
||||
// key
|
||||
check(s[pos] == '"') { "expected '\"' at pos $pos in: $s" }
|
||||
pos++
|
||||
val keyEnd = s.indexOf('"', pos)
|
||||
check(keyEnd > 0) { "unterminated string at pos $pos" }
|
||||
val key = unescapeJson(s.substring(pos, keyEnd))
|
||||
pos = keyEnd + 1
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
check(s[pos] == ':') { "expected ':' at pos $pos" }
|
||||
pos++
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
// value
|
||||
val (value, newPos) = parseValue(s, pos)
|
||||
result[key] = value
|
||||
pos = newPos
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
if (pos < s.length && s[pos] == ',') pos++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private fun parseValue(s: String, start: Int): Pair<Any?, Int> {
|
||||
var pos = start
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
return when {
|
||||
pos >= s.length -> null to pos
|
||||
s[pos] == '"' -> parseStringValue(s, pos)
|
||||
s[pos] == '{' -> parseMapValue(s, pos)
|
||||
s[pos] == '[' -> parseListValue(s, pos)
|
||||
s.startsWith("null", pos) -> null to (pos + 4)
|
||||
s.startsWith("true", pos) -> true to (pos + 4)
|
||||
s.startsWith("false", pos) -> false to (pos + 5)
|
||||
else -> parseNumberValue(s, pos)
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseStringValue(s: String, start: Int): Pair<String, Int> {
|
||||
check(s[start] == '"')
|
||||
pos@ for (pos in (start + 1) until s.length) {
|
||||
when (s[pos]) {
|
||||
'\\' -> continue@pos // skip next char
|
||||
'"' -> return unescapeJson(s.substring(start + 1, pos)) to (pos + 1)
|
||||
}
|
||||
}
|
||||
error("unterminated string in JSON")
|
||||
}
|
||||
|
||||
private fun parseMapValue(s: String, start: Int): Pair<Map<String, Any?>, Int> {
|
||||
check(s[start] == '{')
|
||||
var depth = 1
|
||||
var idx = start + 1
|
||||
while (idx < s.length) {
|
||||
when (s[idx]) {
|
||||
'{' -> depth++
|
||||
'}' -> {
|
||||
depth--
|
||||
if (depth == 0)
|
||||
return parsePairs(s.substring(start + 1, idx)) to (idx + 1)
|
||||
}
|
||||
'"' -> {
|
||||
// skip string content
|
||||
var p = idx + 1
|
||||
while (p < s.length) {
|
||||
when (s[p]) {
|
||||
'\\' -> p += 2
|
||||
'"' -> { idx = p; break }
|
||||
else -> p++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
idx++
|
||||
}
|
||||
error("unterminated JSON object")
|
||||
}
|
||||
|
||||
private fun parseListValue(s: String, start: Int): Pair<List<Any?>, Int> {
|
||||
check(s[start] == '[')
|
||||
val items = mutableListOf<Any?>()
|
||||
var pos = start + 1
|
||||
while (pos < s.length) {
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
if (s[pos] == ']') return items to (pos + 1)
|
||||
val (value, newPos) = parseValue(s, pos)
|
||||
items.add(value)
|
||||
pos = newPos
|
||||
skipWhitespace(s, pos).let { pos = it }
|
||||
if (pos < s.length && s[pos] == ',') pos++
|
||||
}
|
||||
error("unterminated JSON array")
|
||||
}
|
||||
|
||||
private fun parseNumberValue(s: String, start: Int): Pair<Number, Int> {
|
||||
var end = start
|
||||
while (end < s.length && (s[end].isDigit() || s[end] == '-' || s[end] == '+' || s[end] == '.' || s[end] == 'e' || s[end] == 'E'))
|
||||
end++
|
||||
val num = s.substring(start, end)
|
||||
return if (num.contains('.') || num.contains('e') || num.contains('E'))
|
||||
num.toDouble() to end
|
||||
else
|
||||
num.toLong() to end
|
||||
}
|
||||
|
||||
private fun skipWhitespace(s: String, pos: Int): Int {
|
||||
var p = pos
|
||||
while (p < s.length && s[p].isWhitespace()) p++
|
||||
return p
|
||||
}
|
||||
|
||||
private fun unescapeJson(s: String): String {
|
||||
if ('\\' !in s) return s
|
||||
val sb = StringBuilder(s.length)
|
||||
var i = 0
|
||||
while (i < s.length) {
|
||||
if (s[i] == '\\' && i + 1 < s.length) {
|
||||
when (s[i + 1]) {
|
||||
'"' -> { sb.append('"'); i += 2 }
|
||||
'\\' -> { sb.append('\\'); i += 2 }
|
||||
'/' -> { sb.append('/'); i += 2 }
|
||||
'n' -> { sb.append('\n'); i += 2 }
|
||||
'r' -> { sb.append('\r'); i += 2 }
|
||||
't' -> { sb.append('\t'); i += 2 }
|
||||
'u' -> {
|
||||
val hex = s.substring(i + 2, i + 6)
|
||||
sb.append(hex.toInt(16).toChar())
|
||||
i += 6
|
||||
}
|
||||
else -> { sb.append(s[i]); i++ }
|
||||
}
|
||||
} else {
|
||||
sb.append(s[i])
|
||||
i++
|
||||
}
|
||||
}
|
||||
return sb.toString()
|
||||
}
|
||||
|
||||
private fun check(condition: Boolean, lazyMessage: () -> String) {
|
||||
if (!condition) throw IllegalArgumentException(lazyMessage())
|
||||
}
|
||||
|
||||
private fun error(message: String): Nothing = throw IllegalArgumentException(message)
|
||||
}
|
||||
@@ -38,6 +38,8 @@ internal class AstChecker(private val program: Program,
|
||||
} else {
|
||||
if (startSub.parameters.isNotEmpty() || startSub.returntypes.isNotEmpty())
|
||||
errors.err("program entrypoint subroutine can't have parameters and/or return values", startSub.position)
|
||||
if (startSub.isPrivate)
|
||||
errors.err("program entrypoint subroutine 'start' cannot be private", startSub.position)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,26 +102,6 @@ internal class AstChecker(private val program: Program,
|
||||
errors.undefined(identifier.nameInSource, identifier.firstTarget(program.builtinFunctions)==null, identifier.position)
|
||||
}
|
||||
else {
|
||||
// Check for private symbol access
|
||||
val privateError = when (stmt) {
|
||||
is VarDecl -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private variable '${stmt.scopedName.joinToString(".")}'"
|
||||
else
|
||||
null
|
||||
}
|
||||
is Subroutine -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private subroutine '${stmt.scopedName.joinToString(".")}'"
|
||||
else
|
||||
null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
if (privateError != null) {
|
||||
errors.err("cannot access $privateError from outside its block", identifier.position)
|
||||
}
|
||||
|
||||
val target = stmt as? VarDecl
|
||||
if (target != null && target.origin == VarDeclOrigin.SUBROUTINEPARAM) {
|
||||
if (target.definingSubroutine!!.isAsmSubroutine) {
|
||||
@@ -137,15 +119,13 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
}
|
||||
|
||||
private fun isAccessWithinSameBlock(identifier: IdentifierReference, targetBlock: Block): Boolean {
|
||||
var current: Node? = identifier.parent
|
||||
while (current != null) {
|
||||
if (current is Block) {
|
||||
return current === targetBlock
|
||||
}
|
||||
current = current.parent
|
||||
private fun checkPrivateTypeAccess(type: DataType, position: Position, currentBlock: Block) {
|
||||
val struct = type.subType
|
||||
if(struct is StructDecl && struct.isPrivate) {
|
||||
val structBlock = struct.definingBlock
|
||||
if(currentBlock !== structBlock)
|
||||
errors.err("cannot access private struct '${struct.scopedName.joinToString(".")}' from outside its block", position)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun visit(unrollLoop: UnrollLoop) {
|
||||
@@ -507,6 +487,16 @@ internal class AstChecker(private val program: Program,
|
||||
if(address != null && address !is NumericLiteral)
|
||||
err("address must be a constant")
|
||||
|
||||
val currentBlock = subroutine.definingBlock
|
||||
subroutine.parameters.forEach { param ->
|
||||
if(param.type.isPointer || param.type.isStructInstance)
|
||||
checkPrivateTypeAccess(param.type, subroutine.position, currentBlock)
|
||||
}
|
||||
subroutine.returntypes.forEach { rt ->
|
||||
if(rt.isPointer || rt.isStructInstance)
|
||||
checkPrivateTypeAccess(rt, subroutine.position, currentBlock)
|
||||
}
|
||||
|
||||
super.visit(subroutine)
|
||||
|
||||
// subroutine must contain at least one 'return' or 'goto'
|
||||
@@ -959,9 +949,13 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
|
||||
override fun visit(decl: VarDecl) {
|
||||
|
||||
if(decl.names.size>1)
|
||||
throw InternalCompilerException("vardecls with multiple names should have been converted into individual vardecls")
|
||||
|
||||
if(decl.datatype.isPointer || decl.datatype.isStructInstance)
|
||||
checkPrivateTypeAccess(decl.datatype, decl.position, decl.definingBlock)
|
||||
|
||||
if(decl.type!=VarDeclType.CONST && "::" in decl.name)
|
||||
errors.err("only enum members can be accessed with '::' syntax", decl.position)
|
||||
|
||||
@@ -1759,6 +1753,12 @@ internal class AstChecker(private val program: Program,
|
||||
if(!typecast.expression.inferType(program).isKnown)
|
||||
errors.err("this expression doesn't return a value", typecast.expression.position)
|
||||
|
||||
if(typecast.type.isPointer || typecast.type.isStructInstance) {
|
||||
val currentBlock = findParentNode<Block>(typecast)
|
||||
if(currentBlock!=null)
|
||||
checkPrivateTypeAccess(typecast.type, typecast.position, currentBlock)
|
||||
}
|
||||
|
||||
val cv = typecast.expression.constValue(program)
|
||||
if(cv != null) {
|
||||
if(typecast.type.isBasic) {
|
||||
@@ -1862,7 +1862,7 @@ internal class AstChecker(private val program: Program,
|
||||
checkPointer(binexpr.left as IdentifierReference)
|
||||
}
|
||||
|
||||
if(builtinFunctionName=="memory") {
|
||||
if(builtinFunctionName=="memory" || builtinFunctionName=="memory__ref") {
|
||||
errors.err("memory() function call should have been desugared into dedicated AST nodes", functionCallExpr.position)
|
||||
}
|
||||
|
||||
@@ -1870,7 +1870,7 @@ internal class AstChecker(private val program: Program,
|
||||
}
|
||||
|
||||
override fun visit(functionCallStatement: FunctionCallStatement) {
|
||||
if(functionCallStatement.isMemoryCall) {
|
||||
if(functionCallStatement.isMemoryCall || functionCallStatement.isMemoryRefCall) {
|
||||
errors.err("memory() function call should have been desugared into dedicated AST nodes", functionCallStatement.position)
|
||||
}
|
||||
|
||||
|
||||
@@ -123,6 +123,12 @@ internal fun Program.verifyFunctionArgTypes(errors: IErrorReporter, options: Com
|
||||
fixer.visit(this)
|
||||
}
|
||||
|
||||
internal fun Program.checkPrivateAccess(errors: IErrorReporter) {
|
||||
val checker = PrivateAccessChecker(this, errors)
|
||||
checker.visit(this)
|
||||
}
|
||||
|
||||
|
||||
internal fun Program.preprocessAst(errors: IErrorReporter, options: CompilationOptions) {
|
||||
val mergeBlocks = BlockMerger(errors)
|
||||
mergeBlocks.visit(this)
|
||||
|
||||
@@ -179,6 +179,13 @@ internal class AstIdentifiersChecker(private val errors: IErrorReporter,
|
||||
super.visit(subroutine)
|
||||
}
|
||||
|
||||
override fun visit(alias: Alias) {
|
||||
val existing = alias.definingScope.lookup(listOf(alias.alias))
|
||||
if(existing != null && existing !== alias)
|
||||
nameError(alias.alias, alias.position, existing)
|
||||
super.visit(alias)
|
||||
}
|
||||
|
||||
override fun visit(label: Label) {
|
||||
if(label.name in BuiltinFunctions) {
|
||||
errors.err("builtin function cannot be redefined", label.position)
|
||||
|
||||
@@ -356,6 +356,10 @@ class AstPreprocessor(val program: Program,
|
||||
|
||||
checkStringParam(functionCallExpr as IFunctionCall, stmtOfExpression)
|
||||
|
||||
if (functionCallExpr.isMemoryCall && functionCallExpr.args.size == 1) {
|
||||
functionCallExpr.target = IdentifierReference(listOf("memory__ref"), functionCallExpr.target.position).apply { linkParents(functionCallExpr) }
|
||||
}
|
||||
|
||||
if(functionCallExpr.target.nameInSource==listOf("sizeof")) {
|
||||
val arg = functionCallExpr.args.firstOrNull()
|
||||
if (arg is PtrDereference) {
|
||||
@@ -373,6 +377,11 @@ class AstPreprocessor(val program: Program,
|
||||
|
||||
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<AstModification> {
|
||||
checkStringParam(functionCallStatement as IFunctionCall, functionCallStatement)
|
||||
|
||||
if (functionCallStatement.isMemoryCall && functionCallStatement.args.size == 1) {
|
||||
functionCallStatement.target = IdentifierReference(listOf("memory__ref"), functionCallStatement.target.position).apply { linkParents(functionCallStatement) }
|
||||
}
|
||||
|
||||
return noModifications
|
||||
}
|
||||
|
||||
@@ -400,15 +409,15 @@ class AstPreprocessor(val program: Program,
|
||||
val replacement = if(tgt2!=null) {
|
||||
if(tgt2 is BuiltinFunctionPlaceholder) {
|
||||
val unscopedTarget = IdentifierReference(listOf(tgt2.name), alias.position)
|
||||
Alias(alias.alias, unscopedTarget, alias.position)
|
||||
Alias(alias.alias, unscopedTarget, alias.isPrivate, alias.position)
|
||||
} else if(tgt2.scopedName != chainedTargetName.nameInSource) {
|
||||
val scopedTarget = IdentifierReference(tgt2.scopedName, alias.position)
|
||||
Alias(alias.alias, scopedTarget, alias.position)
|
||||
Alias(alias.alias, scopedTarget, alias.isPrivate, alias.position)
|
||||
} else {
|
||||
Alias(alias.alias, chainedTargetName, alias.position)
|
||||
Alias(alias.alias, chainedTargetName, alias.isPrivate, alias.position)
|
||||
}
|
||||
} else {
|
||||
Alias(alias.alias, chainedTargetName, alias.position)
|
||||
Alias(alias.alias, chainedTargetName, alias.isPrivate, alias.position)
|
||||
}
|
||||
return listOf(AstReplaceNode(alias, replacement, parent))
|
||||
}
|
||||
@@ -469,6 +478,10 @@ class AstPreprocessor(val program: Program,
|
||||
}
|
||||
|
||||
override fun after(enum: Enumeration, parent: Node): Iterable<AstModification> {
|
||||
if (enum.members.isEmpty()) {
|
||||
errors.err("enumeration must contain at least one member", enum.position)
|
||||
}
|
||||
|
||||
// first check that there is no name conflict
|
||||
(parent as? IStatementContainer)?.let {
|
||||
val allNamed = it.statements.asSequence()
|
||||
@@ -510,6 +523,7 @@ class AstPreprocessor(val program: Program,
|
||||
.names(membername)
|
||||
.type(VarDeclType.CONST)
|
||||
.value(membervalue)
|
||||
.isPrivate(enum.isPrivate)
|
||||
.build()
|
||||
}
|
||||
|
||||
|
||||
@@ -26,18 +26,18 @@ internal class CodeDesugarer(val program: Program, private val target: ICompilat
|
||||
// - replace ptr^^ by @(ptr) if ptr is just an uword.
|
||||
// - replace p1^^ = p2^^ by memcopy.
|
||||
|
||||
private val globalReservedSlabs = mutableSetOf<String>()
|
||||
private val globalReservedSlabs = mutableMapOf<String, MemorySlabReservation>()
|
||||
|
||||
override fun before(program: Program): Iterable<AstModification> {
|
||||
globalReservedSlabs.clear()
|
||||
// Pre-collect existing reservations from the entire program to avoid duplicates in multiple passes
|
||||
val collector = object : IAstVisitor {
|
||||
override fun visit(reservation: MemorySlabReservation) {
|
||||
globalReservedSlabs.add(reservation.slabName)
|
||||
globalReservedSlabs[reservation.slabName] = reservation
|
||||
}
|
||||
}
|
||||
program.modules.forEach { module ->
|
||||
module.statements.forEach { it.accept(collector) }
|
||||
module.accept(collector)
|
||||
}
|
||||
return super.before(program)
|
||||
}
|
||||
@@ -262,6 +262,20 @@ _after:
|
||||
}
|
||||
|
||||
override fun after(functionCallExpr: FunctionCallExpression, parent: Node): Iterable<AstModification> {
|
||||
if (functionCallExpr.isMemoryRefCall) {
|
||||
val str = functionCallExpr.args[0] as? StringLiteral
|
||||
if (str == null) {
|
||||
errors.err("memory name argument must be a string literal", functionCallExpr.args[0].position)
|
||||
return noModifications
|
||||
} else if (str.value.isEmpty()) {
|
||||
errors.err("memory name argument cannot be empty string", functionCallExpr.args[0].position)
|
||||
return noModifications
|
||||
}
|
||||
val slabName = sanitizeSlabName(str.value)
|
||||
val ref = MemorySlabRef(slabName, functionCallExpr.position)
|
||||
return listOf(AstReplaceNode(functionCallExpr, ref, parent))
|
||||
}
|
||||
|
||||
if (functionCallExpr.isMemoryCall) {
|
||||
val str = functionCallExpr.args[0] as? StringLiteral
|
||||
if (str == null) {
|
||||
@@ -297,9 +311,10 @@ _after:
|
||||
val container = containingStmt.parent as IStatementContainer
|
||||
|
||||
val mods = mutableListOf<AstModification>(AstReplaceNode(functionCallExpr, ref, parent))
|
||||
if (slabName !in globalReservedSlabs) {
|
||||
val existing = globalReservedSlabs[slabName]
|
||||
if (existing == null || existing.size != size || existing.align != align) {
|
||||
mods.add(AstInsert.before(containingStmt, reservation, container))
|
||||
globalReservedSlabs.add(slabName)
|
||||
globalReservedSlabs[slabName] = reservation
|
||||
}
|
||||
return mods
|
||||
}
|
||||
@@ -307,6 +322,11 @@ _after:
|
||||
}
|
||||
|
||||
override fun after(functionCallStatement: FunctionCallStatement, parent: Node): Iterable<AstModification> {
|
||||
if (functionCallStatement.isMemoryRefCall) {
|
||||
val container = parent as IStatementContainer
|
||||
return listOf(AstRemove(functionCallStatement, container))
|
||||
}
|
||||
|
||||
if (functionCallStatement.isMemoryCall) {
|
||||
val str = functionCallStatement.args[0] as? StringLiteral
|
||||
if (str == null) {
|
||||
@@ -336,11 +356,12 @@ _after:
|
||||
val align = alignNum.toUInt()
|
||||
|
||||
val container = parent as IStatementContainer
|
||||
return if (slabName in globalReservedSlabs) {
|
||||
val existing = globalReservedSlabs[slabName]
|
||||
return if (existing != null && existing.size == size && existing.align == align) {
|
||||
listOf(AstRemove(functionCallStatement, container))
|
||||
} else {
|
||||
globalReservedSlabs.add(slabName)
|
||||
val reservation = MemorySlabReservation(slabName, size, align, functionCallStatement.position)
|
||||
globalReservedSlabs[slabName] = reservation
|
||||
listOf(AstReplaceNode(functionCallStatement, reservation, parent))
|
||||
}
|
||||
}
|
||||
|
||||
+25
-1
@@ -20,7 +20,7 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
val parentFunc = (string.parent as? IFunctionCall)?.target
|
||||
if(parentFunc!=null) {
|
||||
val parentCall = string.parent as? IFunctionCall
|
||||
if(parentCall!=null && parentCall.isMemoryCall) {
|
||||
if(parentCall!=null && (parentCall.isMemoryCall || parentCall.isMemoryRefCall)) {
|
||||
// memory() builtin function just uses the string as a label name
|
||||
return noModifications
|
||||
}
|
||||
@@ -149,6 +149,14 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
// First component might be an alias
|
||||
val tgt2 = identifier.definingScope.lookup(identifier.nameInSource[0]) as? Alias
|
||||
if(tgt2!=null && parent !is Alias) {
|
||||
if(tgt2.isPrivate) {
|
||||
val referencingBlock = findParentNode<Block>(identifier)
|
||||
val aliasBlock = findParentNode<Block>(tgt2)
|
||||
if(referencingBlock!=null && aliasBlock!=null && referencingBlock!==aliasBlock) {
|
||||
errors.err("cannot access private alias '${tgt2.alias}' from outside its block", identifier.position)
|
||||
return noModifications
|
||||
}
|
||||
}
|
||||
val aliasTarget = resolveAliasTarget(tgt2)
|
||||
if(aliasTarget != null) {
|
||||
val actual = when(aliasTarget) {
|
||||
@@ -161,6 +169,14 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
}
|
||||
|
||||
if(target is Alias && parent !is Alias) {
|
||||
if(target.isPrivate) {
|
||||
val referencingBlock = findParentNode<Block>(identifier)
|
||||
val aliasBlock = findParentNode<Block>(target)
|
||||
if(referencingBlock!=null && aliasBlock!=null && referencingBlock!==aliasBlock) {
|
||||
errors.err("cannot access private alias '${target.alias}' from outside its block", identifier.position)
|
||||
return noModifications
|
||||
}
|
||||
}
|
||||
val targetStatement = resolveAliasTarget(target)
|
||||
if(targetStatement == null) {
|
||||
return noModifications // alias loop or unresolved
|
||||
@@ -220,6 +236,14 @@ internal class LiteralsToAutoVarsAndRecombineIdentifiers(private val program: Pr
|
||||
if(deref.chain.isEmpty()) return emptyList()
|
||||
val tgt2 = deref.definingScope.lookup(deref.chain[0]) as? Alias
|
||||
if(tgt2!=null && parent !is Alias) {
|
||||
if(tgt2.isPrivate) {
|
||||
val referencingBlock = findParentNode<Block>(deref)
|
||||
val aliasBlock = findParentNode<Block>(tgt2)
|
||||
if(referencingBlock!=null && aliasBlock!=null && referencingBlock!==aliasBlock) {
|
||||
errors.err("cannot access private alias '${tgt2.alias}' from outside its block", deref.position)
|
||||
return noModifications
|
||||
}
|
||||
}
|
||||
val aliasTarget = resolveAliasTarget(tgt2)
|
||||
if(aliasTarget != null) {
|
||||
val unaliased = when(aliasTarget) {
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package prog8.compiler.astprocessing
|
||||
|
||||
import prog8.ast.Node
|
||||
import prog8.ast.ParentSentinel
|
||||
import prog8.ast.Program
|
||||
import prog8.ast.expressions.BinaryExpression
|
||||
import prog8.ast.expressions.IdentifierReference
|
||||
import prog8.ast.statements.*
|
||||
import prog8.ast.walk.IAstVisitor
|
||||
import prog8.code.core.IErrorReporter
|
||||
|
||||
internal class PrivateAccessChecker(
|
||||
private val program: Program,
|
||||
private val errors: IErrorReporter
|
||||
) : IAstVisitor {
|
||||
|
||||
override fun visit(identifier: IdentifierReference) {
|
||||
val parentExpr = identifier.parent as? BinaryExpression
|
||||
if (parentExpr?.operator == ".") {
|
||||
return // identifiers will be checked over at the BinaryExpression itself
|
||||
}
|
||||
|
||||
val stmt = identifier.targetStatement(program.builtinFunctions)
|
||||
if (stmt != null) {
|
||||
val privateError = when (stmt) {
|
||||
is VarDecl -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private variable '${stmt.scopedName.joinToString(".")}'"
|
||||
else null
|
||||
}
|
||||
is Subroutine -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private subroutine '${stmt.scopedName.joinToString(".")}'"
|
||||
else null
|
||||
}
|
||||
is StructDecl -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private struct '${stmt.scopedName.joinToString(".")}'"
|
||||
else null
|
||||
}
|
||||
is Enumeration -> {
|
||||
if (stmt.isPrivate && !isAccessWithinSameBlock(identifier, stmt.definingBlock))
|
||||
"private enum '${stmt.scopedName.joinToString(".")}'"
|
||||
else null
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
if (privateError != null) {
|
||||
errors.err("cannot access $privateError from outside its block", identifier.position)
|
||||
}
|
||||
}
|
||||
|
||||
super.visit(identifier)
|
||||
}
|
||||
|
||||
private fun isAccessWithinSameBlock(identifier: IdentifierReference, definingBlock: Block): Boolean {
|
||||
var node: Node = identifier
|
||||
while (node !is Block) {
|
||||
node = node.parent
|
||||
if (node is ParentSentinel)
|
||||
return false
|
||||
}
|
||||
return node === definingBlock
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,14 @@ internal class VerifyFunctionArgTypes(val program: Program, val options: Compila
|
||||
}
|
||||
}
|
||||
|
||||
// check for missing reservations
|
||||
val slabNames = memorySlabs.map { it.name }.toSet()
|
||||
for (ref in memorySlabRefs) {
|
||||
if (ref.slabName !in slabNames) {
|
||||
errors.err("missing memory reservation with name '${ref.slabName}'", ref.position)
|
||||
}
|
||||
}
|
||||
|
||||
// remove unused strings from interned strings block
|
||||
val internedBlock = program.allBlocks.singleOrNull { it.name == INTERNED_STRINGS_MODULENAME }
|
||||
internedBlock?.statements?.withIndex()?.reversed()?.forEach { (index, st) ->
|
||||
@@ -40,12 +48,14 @@ internal class VerifyFunctionArgTypes(val program: Program, val options: Compila
|
||||
|
||||
private class Slab(val name: String, val size: Int, val align: Int, val position: Position)
|
||||
private val memorySlabs = mutableListOf<Slab>()
|
||||
private val memorySlabRefs = mutableListOf<MemorySlabRef>()
|
||||
|
||||
override fun visit(reservation: MemorySlabReservation) {
|
||||
memorySlabs.add(Slab(reservation.slabName, reservation.size.toInt(), reservation.align.toInt(), reservation.position))
|
||||
}
|
||||
|
||||
override fun visit(ref: MemorySlabRef) {
|
||||
memorySlabRefs.add(ref)
|
||||
}
|
||||
|
||||
override fun visit(functionCallExpr: FunctionCallExpression) {
|
||||
|
||||
@@ -65,7 +65,7 @@ class TestCompilerOnCharLit: FunSpec({
|
||||
main {
|
||||
extsub $FFD2 = chrout(ubyte ch @ A)
|
||||
sub start() {
|
||||
ubyte ch = '\n'
|
||||
ubyte @shared ch = '\n'
|
||||
chrout(ch)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,14 +156,14 @@ class TestCompilerOnExamplesC64: FunSpec({
|
||||
ExampleSizes("balloonflight", c64SizeOptimized=1674, c64SizeUnoptimized=1745),
|
||||
ExampleSizes("banking", c64SizeOptimized=680, c64SizeUnoptimized=1451),
|
||||
ExampleSizes("bdmusic-irq", c64SizeOptimized=1001, c64SizeUnoptimized=1019),
|
||||
ExampleSizes("bdmusic", c64SizeOptimized=944, c64SizeUnoptimized=1011),
|
||||
ExampleSizes("charset", c64SizeOptimized=841, c64SizeUnoptimized=860),
|
||||
ExampleSizes("bdmusic", c64SizeOptimized=938, c64SizeUnoptimized=1011),
|
||||
ExampleSizes("charset", c64SizeOptimized=835, c64SizeUnoptimized=860),
|
||||
ExampleSizes("cube3d-sprites", c64SizeOptimized=2712, c64SizeUnoptimized=2764),
|
||||
ExampleSizes("fileselector", c64SizeOptimized=3568, c64SizeUnoptimized=4758),
|
||||
ExampleSizes("library/main", c64SizeOptimized=321, c64SizeUnoptimized=1237),
|
||||
ExampleSizes("multiplexer", c64SizeOptimized=1356, c64SizeUnoptimized=1507),
|
||||
ExampleSizes("petswirl", c64SizeOptimized=959, c64SizeUnoptimized=1029),
|
||||
ExampleSizes("plasma", c64SizeOptimized=1696, c64SizeUnoptimized=1731),
|
||||
ExampleSizes("plasma", c64SizeOptimized=1690, c64SizeUnoptimized=1731),
|
||||
ExampleSizes("rasterbars", c64SizeOptimized=845, c64SizeUnoptimized=892),
|
||||
ExampleSizes("simplemultiplexer", c64SizeOptimized=843, c64SizeUnoptimized=914),
|
||||
ExampleSizes("sprites", c64SizeOptimized=963, c64SizeUnoptimized=1052),
|
||||
@@ -225,34 +225,35 @@ class TestCompilerOnExamplesCx16: FunSpec({
|
||||
ExampleSizes("balloonflight", cx16SizeOptimized=3187, cx16SizeUnoptimized=4163),
|
||||
ExampleSizes("bdmusic", cx16SizeOptimized=2386, cx16SizeUnoptimized=2761),
|
||||
ExampleSizes("bobs", cx16SizeOptimized=2800, cx16SizeUnoptimized=3414),
|
||||
ExampleSizes("bubbleuniverse", cx16SizeOptimized=1449, cx16SizeUnoptimized=1958),
|
||||
ExampleSizes("bubbleuniverse", cx16SizeOptimized=1450, cx16SizeUnoptimized=1958),
|
||||
ExampleSizes("charfade", cx16SizeOptimized=1054, cx16SizeUnoptimized=1362),
|
||||
ExampleSizes("charsets", cx16SizeOptimized=1125, cx16SizeUnoptimized=1417),
|
||||
ExampleSizes("circles", cx16SizeOptimized=2392, cx16SizeUnoptimized=4547),
|
||||
ExampleSizes("circles", cx16SizeOptimized=2385, cx16SizeUnoptimized=4547),
|
||||
ExampleSizes("cobramk3-gfx", cx16SizeOptimized=7705, cx16SizeUnoptimized=10612),
|
||||
ExampleSizes("colorbars", cx16SizeOptimized=1606, cx16SizeUnoptimized=1904),
|
||||
ExampleSizes("cxlogo", cx16SizeOptimized=599, cx16SizeUnoptimized=895),
|
||||
ExampleSizes("diskspeed", cx16SizeOptimized=6280, cx16SizeUnoptimized=8283),
|
||||
ExampleSizes("fileseek", cx16SizeOptimized=2474, cx16SizeUnoptimized=4496),
|
||||
ExampleSizes("fileseek", cx16SizeOptimized=2468, cx16SizeUnoptimized=4496),
|
||||
ExampleSizes("floatparse", cx16SizeOptimized=8303, cx16SizeUnoptimized=9190),
|
||||
ExampleSizes("interpolation", cx16SizeOptimized=3791, cx16SizeUnoptimized=4768),
|
||||
ExampleSizes("kefrenbars", cx16SizeOptimized=2392, cx16SizeUnoptimized=4654),
|
||||
ExampleSizes("kefrenbars", cx16SizeOptimized=2386, cx16SizeUnoptimized=4654),
|
||||
ExampleSizes("keyboardhandler", cx16SizeOptimized=412, cx16SizeUnoptimized=717),
|
||||
ExampleSizes("landscape", cx16SizeOptimized=1699, cx16SizeUnoptimized=1997),
|
||||
ExampleSizes("life", cx16SizeOptimized=1304, cx16SizeUnoptimized=1751),
|
||||
ExampleSizes("landscape", cx16SizeOptimized=1673, cx16SizeUnoptimized=1997),
|
||||
ExampleSizes("life", cx16SizeOptimized=1320, cx16SizeUnoptimized=1751),
|
||||
ExampleSizes("mandelbrot", cx16SizeOptimized=1120, cx16SizeUnoptimized=1434),
|
||||
ExampleSizes("multi-irq-new", cx16SizeOptimized=1073, cx16SizeUnoptimized=1519),
|
||||
ExampleSizes("multi-irq-old", cx16SizeOptimized=962, cx16SizeUnoptimized=1410),
|
||||
ExampleSizes("plasma", cx16SizeOptimized=1739, cx16SizeUnoptimized=2066),
|
||||
ExampleSizes("rasterbars", cx16SizeOptimized=1268, cx16SizeUnoptimized=1685),
|
||||
ExampleSizes("serialdownload", cx16SizeOptimized=2471, cx16SizeUnoptimized=3649),
|
||||
ExampleSizes("showbmx", cx16SizeOptimized=2745, cx16SizeUnoptimized=5269),
|
||||
ExampleSizes("snow", cx16SizeOptimized=3127, cx16SizeUnoptimized=5295),
|
||||
ExampleSizes("snow", cx16SizeOptimized=3120, cx16SizeUnoptimized=5295),
|
||||
ExampleSizes("sortingbench", cx16SizeOptimized=2928, cx16SizeUnoptimized=3847),
|
||||
ExampleSizes("spotlight", cx16SizeOptimized=908, cx16SizeUnoptimized=1364),
|
||||
ExampleSizes("starszoom", cx16SizeOptimized=2406, cx16SizeUnoptimized=4863),
|
||||
ExampleSizes("starszoom", cx16SizeOptimized=2399, cx16SizeUnoptimized=4863),
|
||||
ExampleSizes("test_gfx_hires", cx16SizeOptimized=4835, cx16SizeUnoptimized=7514),
|
||||
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5863, cx16SizeUnoptimized=7188),
|
||||
ExampleSizes("testmonogfx", cx16SizeOptimized=10494, cx16SizeUnoptimized=11982),
|
||||
ExampleSizes("test_gfx_lores", cx16SizeOptimized=5841, cx16SizeUnoptimized=7188),
|
||||
ExampleSizes("testmonogfx", cx16SizeOptimized=10463, cx16SizeUnoptimized=11982),
|
||||
ExampleSizes("textspotlight", cx16SizeOptimized=3431, cx16SizeUnoptimized=3788),
|
||||
),
|
||||
listOf(false, true)
|
||||
@@ -285,14 +286,14 @@ class TestCompilerOnExamplesBothC64andCx16: FunSpec({
|
||||
val bothCx16AndC64 = cartesianProduct(
|
||||
listOf(
|
||||
ExampleSizes("balls", c64SizeOptimized=885, c64SizeUnoptimized=919, cx16SizeOptimized=874, cx16SizeUnoptimized=1188),
|
||||
ExampleSizes("boingball", c64SizeOptimized=1484, c64SizeUnoptimized=1544, cx16SizeOptimized=1486, cx16SizeUnoptimized=1962),
|
||||
ExampleSizes("boingball", c64SizeOptimized=1478, c64SizeUnoptimized=1544, cx16SizeOptimized=1486, cx16SizeUnoptimized=1957),
|
||||
ExampleSizes("cube3d", c64SizeOptimized=2444, c64SizeUnoptimized=2509, cx16SizeOptimized=2381, cx16SizeUnoptimized=2712),
|
||||
ExampleSizes("cube3d-float", c64SizeOptimized=2619, c64SizeUnoptimized=2668, cx16SizeOptimized=2511, cx16SizeUnoptimized=2835),
|
||||
ExampleSizes("cube3d-gfx", c64SizeOptimized=3536, c64SizeUnoptimized=3589, cx16SizeOptimized=2206, cx16SizeUnoptimized=2522),
|
||||
ExampleSizes("dirlist", c64SizeOptimized=1543, c64SizeUnoptimized=2405, cx16SizeOptimized=1525, cx16SizeUnoptimized=3372),
|
||||
ExampleSizes("fibonacci", c64SizeOptimized=570, c64SizeUnoptimized=585, cx16SizeOptimized=626, cx16SizeUnoptimized=925),
|
||||
ExampleSizes("fractal-tree", c64SizeOptimized=2808, c64SizeUnoptimized=2845, cx16SizeOptimized=1220, cx16SizeUnoptimized=1511),
|
||||
ExampleSizes("maze", c64SizeOptimized=3100, c64SizeUnoptimized=3189, cx16SizeOptimized=2873, cx16SizeUnoptimized=3251),
|
||||
ExampleSizes("maze", c64SizeOptimized=3088, c64SizeUnoptimized=3189, cx16SizeOptimized=2873, cx16SizeUnoptimized=3251),
|
||||
ExampleSizes("mandelbrot", c64SizeOptimized=1138, c64SizeUnoptimized=1172, cx16SizeOptimized=1120, cx16SizeUnoptimized=1434),
|
||||
ExampleSizes("mandelbrot-gfx", c64SizeOptimized=1457, c64SizeUnoptimized=1479, cx16SizeOptimized=957, cx16SizeUnoptimized=1250),
|
||||
ExampleSizes("multitasking", c64SizeOptimized=1956, c64SizeUnoptimized=2029, cx16SizeOptimized=1826, cx16SizeUnoptimized=2159),
|
||||
@@ -302,12 +303,12 @@ class TestCompilerOnExamplesBothC64andCx16: FunSpec({
|
||||
ExampleSizes("screencodes", c64SizeOptimized=667, c64SizeUnoptimized=687, cx16SizeOptimized=655, cx16SizeUnoptimized=959),
|
||||
ExampleSizes("swirl", c64SizeOptimized=828, c64SizeUnoptimized=831, cx16SizeOptimized=923, cx16SizeUnoptimized=1208),
|
||||
ExampleSizes("swirl-float", c64SizeOptimized=708, c64SizeUnoptimized=735, cx16SizeOptimized=595, cx16SizeUnoptimized=906),
|
||||
ExampleSizes("tehtriz", c64SizeOptimized=4339, c64SizeUnoptimized=4504, cx16SizeOptimized=6211, cx16SizeUnoptimized=6742),
|
||||
ExampleSizes("textelite", c64SizeOptimized=11440, c64SizeUnoptimized=12423, cx16SizeOptimized=10778, cx16SizeUnoptimized=13051),
|
||||
ExampleSizes("tehtriz", c64SizeOptimized=4333, c64SizeUnoptimized=4504, cx16SizeOptimized=6211, cx16SizeUnoptimized=6742),
|
||||
ExampleSizes("textelite", c64SizeOptimized=11440, c64SizeUnoptimized=12423, cx16SizeOptimized=10778, cx16SizeUnoptimized=13046),
|
||||
ExampleSizes("pointers/animalgame", c64SizeOptimized=1570, c64SizeUnoptimized=2126, cx16SizeOptimized=1605, cx16SizeUnoptimized=2412),
|
||||
ExampleSizes("pointers/binarytree", c64SizeOptimized=2552, c64SizeUnoptimized=2640, cx16SizeOptimized=2085, cx16SizeUnoptimized=2389),
|
||||
ExampleSizes("pointers/hashtable", c64SizeOptimized=3288, c64SizeUnoptimized=4140, cx16SizeOptimized=2832, cx16SizeUnoptimized=3786),
|
||||
ExampleSizes("pointers/sortedlist", c64SizeOptimized=1268, c64SizeUnoptimized=1319, cx16SizeOptimized=1223, cx16SizeUnoptimized=1532),
|
||||
ExampleSizes("pointers/binarytree", c64SizeOptimized=2562, c64SizeUnoptimized=2640, cx16SizeOptimized=2085, cx16SizeUnoptimized=2389),
|
||||
ExampleSizes("pointers/hashtable", c64SizeOptimized=3294, c64SizeUnoptimized=4140, cx16SizeOptimized=2832, cx16SizeUnoptimized=3786),
|
||||
ExampleSizes("pointers/sortedlist", c64SizeOptimized=1274, c64SizeUnoptimized=1319, cx16SizeOptimized=1223, cx16SizeUnoptimized=1532),
|
||||
ExampleSizes("pointers/sorting", c64SizeOptimized=1776, c64SizeUnoptimized=2676, cx16SizeOptimized=1788, cx16SizeUnoptimized=2659),
|
||||
),
|
||||
listOf(false, true),
|
||||
|
||||
@@ -28,10 +28,8 @@ class TestLookupSemantics: FunSpec({
|
||||
|
||||
fun compileAndGetSymbolTable(source: String): SymbolTable {
|
||||
val errors = ErrorReporterForTests()
|
||||
val result = compileText(C64Target(), false, source, outputDir, errors = errors, writeAssembly = true)
|
||||
if(result == null) {
|
||||
throw AssertionError("Compilation failed: ${errors.errors.joinToString("; ")}")
|
||||
}
|
||||
val result = compileText(C64Target(), false, source, outputDir, errors = errors)
|
||||
?: throw AssertionError("Compilation failed: ${errors.errors.joinToString("; ")}")
|
||||
return result.codegenSymboltable
|
||||
?: throw AssertionError("No symbol table available")
|
||||
}
|
||||
@@ -53,9 +51,9 @@ class TestLookupSemantics: FunSpec({
|
||||
test("qualified lookup always starts from root") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 10
|
||||
uword @shared value = 10
|
||||
sub start() {
|
||||
uword result = main.value
|
||||
uword @shared result = main.value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -66,9 +64,9 @@ class TestLookupSemantics: FunSpec({
|
||||
test("unqualified lookup walks parent chain") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 10
|
||||
uword @shared value = 10
|
||||
sub start() {
|
||||
uword result = value
|
||||
uword @shared result = value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -82,9 +80,9 @@ class TestLookupSemantics: FunSpec({
|
||||
test("qualified lookup with multiple path segments") {
|
||||
val src = """
|
||||
main {
|
||||
uword deep = 100
|
||||
uword @shared deep = 100
|
||||
sub start() {
|
||||
uword result = main.deep
|
||||
uword @shared result = main.deep
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -99,10 +97,10 @@ class TestLookupSemantics: FunSpec({
|
||||
test("local variable shadows parent scope unqualified lookup") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 10
|
||||
uword @shared value = 10
|
||||
sub start() {
|
||||
uword value = 5
|
||||
uword result = value
|
||||
uword @shared value = 5
|
||||
uword @shared result = value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -114,10 +112,10 @@ class TestLookupSemantics: FunSpec({
|
||||
test("qualified lookup unaffected by shadowing") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 10
|
||||
uword @shared value = 10
|
||||
sub start() {
|
||||
uword value = 5
|
||||
uword result = main.value
|
||||
uword @shared value = 5
|
||||
uword @shared result = main.value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -129,11 +127,11 @@ class TestLookupSemantics: FunSpec({
|
||||
test("multiple levels of shadowing") {
|
||||
val src = """
|
||||
main {
|
||||
uword value = 1
|
||||
uword @shared value = 1
|
||||
sub start() {
|
||||
uword value = 2
|
||||
uword v1 = value
|
||||
uword v2 = main.value
|
||||
uword @shared value = 2
|
||||
uword @shared v1 = value
|
||||
uword @shared v2 = main.value
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -149,11 +147,11 @@ class TestLookupSemantics: FunSpec({
|
||||
test("qualified lookup across modules") {
|
||||
val src = """
|
||||
module1 {
|
||||
uword m1_var = 111
|
||||
uword @shared m1_var = 111
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
uword result = module1.m1_var
|
||||
uword @shared result = module1.m1_var
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -178,9 +176,9 @@ class TestLookupSemantics: FunSpec({
|
||||
test("nested block can access outer module symbols") {
|
||||
val src = """
|
||||
main {
|
||||
uword outer = 100
|
||||
uword @shared outer = 100
|
||||
sub start() {
|
||||
uword result = outer
|
||||
uword @shared result = outer
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -197,7 +195,7 @@ class TestLookupSemantics: FunSpec({
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
uword result = abs(-5)
|
||||
uword @shared result = abs(-5)
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -209,8 +207,8 @@ class TestLookupSemantics: FunSpec({
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
uword myabs = 10
|
||||
uword result = myabs
|
||||
uword @shared myabs = 10
|
||||
uword @shared result = myabs
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -232,7 +230,7 @@ class TestLookupSemantics: FunSpec({
|
||||
}
|
||||
sub start() {
|
||||
^^Point ptr
|
||||
word result = ptr.x
|
||||
word @shared result = ptr.x
|
||||
}
|
||||
}
|
||||
"""
|
||||
|
||||
@@ -210,9 +210,9 @@ class TestNumbers: FunSpec({
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
uword uw1 = -32768
|
||||
uword uw = -1
|
||||
ubyte ub = -1
|
||||
uword @shared uw1 = -32768
|
||||
uword @shared uw = -1
|
||||
ubyte @shared ub = -1
|
||||
uw = -2 as uword
|
||||
ub = -2 as ubyte
|
||||
}
|
||||
|
||||
@@ -76,6 +76,26 @@ main {
|
||||
compileText(C64Target(), true, src, outputDir) shouldNotBe null
|
||||
}
|
||||
|
||||
test("do not inline asmsub with parameters") {
|
||||
// Parameterized asmsubs cannot be inlined by the AST Inliner because it doesn't understand
|
||||
// the low-level register mapping (like @A) and cannot generate the necessary code to load
|
||||
// the arguments into those registers.
|
||||
val src="""
|
||||
main {
|
||||
inline asmsub foo(ubyte a @A) {
|
||||
%asm {{
|
||||
inx
|
||||
}}
|
||||
}
|
||||
sub start() {
|
||||
foo(10)
|
||||
}
|
||||
}"""
|
||||
val result = compileText(C64Target(), true, src, outputDir)!!
|
||||
val startSub = result.compilerAst.entrypoint
|
||||
startSub.statements.any { it is FunctionCallStatement && it.target.nameInSource.last() == "foo" } shouldBe true
|
||||
}
|
||||
|
||||
test("don't remove empty subroutine if it's referenced") {
|
||||
val sourcecode = """
|
||||
main {
|
||||
@@ -1693,8 +1713,7 @@ main {
|
||||
hasVoidCall shouldBe false
|
||||
}
|
||||
|
||||
xtest("inline call with one return value and one parameter") {
|
||||
// DISABLED: Requires parameter substitution in expression context (not yet implemented)
|
||||
test("inline call with one return value and one parameter") {
|
||||
// Tests that function calls returning one value with one parameter are inlined
|
||||
// and that the parameter is correctly substituted with the argument
|
||||
val src = """
|
||||
@@ -1724,8 +1743,7 @@ main {
|
||||
binExpr.right shouldBe instanceOf<NumericLiteral>()
|
||||
}
|
||||
|
||||
xtest("inline call with two return values and two parameters") {
|
||||
// DISABLED: Requires parameter substitution for multi-return calls (not yet implemented)
|
||||
test("inline call with two return values and two parameters") {
|
||||
// Tests that function calls returning two values with two parameters are inlined
|
||||
// and that both parameters are correctly substituted
|
||||
val src = """
|
||||
|
||||
@@ -1213,9 +1213,9 @@ main {
|
||||
byte bb
|
||||
}
|
||||
|
||||
^^List l1 = 2000
|
||||
^^List l2 = 3000
|
||||
^^Foo f1 = 4000
|
||||
^^List @shared l1 = 2000
|
||||
^^List @shared l2 = 3000
|
||||
^^Foo @shared f1 = 4000
|
||||
|
||||
l1^^ = l2^^ ; memcpy1
|
||||
l1[3] = l2^^ ; memcpy2
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
package prog8tests.compiler
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import prog8.ast.statements.VarDecl
|
||||
import prog8.ast.statements.VarDeclType
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8tests.helpers.compileText
|
||||
|
||||
class TestPtrConstPromotion : FunSpec({
|
||||
val outputDir = tempdir().toPath()
|
||||
val vmTarget = VMTarget()
|
||||
|
||||
test("pointer variable with 2 dereferences is promoted to const") {
|
||||
val src = """
|
||||
%import textio
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = memory("slab", 10, 1)
|
||||
ubyte a = ptr^^
|
||||
ubyte b = ptr^^
|
||||
txt.print_ub(a)
|
||||
txt.print_ub(b)
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(vmTarget, true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.CONST
|
||||
}
|
||||
|
||||
test("pointer variable with 3 dereferences is NOT promoted to const") {
|
||||
val src = """
|
||||
%import textio
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = memory("slab", 10, 1)
|
||||
ubyte a = ptr^^
|
||||
ubyte b = ptr^^
|
||||
ubyte c = ptr^^
|
||||
txt.print_ub(a)
|
||||
txt.print_ub(b)
|
||||
txt.print_ub(c)
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(vmTarget, true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
|
||||
test("pointer variable with indexing dereferences is handled") {
|
||||
val src = """
|
||||
%import textio
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = memory("slab", 10, 1)
|
||||
ubyte a = ptr[0]
|
||||
ubyte b = ptr[1]
|
||||
ubyte c = ptr[2]
|
||||
txt.print_ub(a)
|
||||
txt.print_ub(b)
|
||||
txt.print_ub(c)
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(vmTarget, true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
})
|
||||
@@ -317,7 +317,7 @@ class TestScoping: FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
byte wrongaddress = 100
|
||||
byte @shared wrongaddress = 100
|
||||
|
||||
goto wrongaddress ; must be uword
|
||||
goto main.routine ; can't take args
|
||||
|
||||
@@ -361,8 +361,8 @@ class TestStringEncodings: FunSpec({
|
||||
str string1 = "default"
|
||||
str string2 = petscii:"petscii"
|
||||
|
||||
ubyte char1 = 'd'
|
||||
ubyte char2 = petscii:'p'
|
||||
ubyte @shared char1 = 'd'
|
||||
ubyte @shared char2 = petscii:'p'
|
||||
|
||||
sub start() {
|
||||
}
|
||||
@@ -389,8 +389,8 @@ class TestStringEncodings: FunSpec({
|
||||
str string1 = "default"
|
||||
str string2 = petscii:"petscii"
|
||||
|
||||
ubyte char1 = 'd'
|
||||
ubyte char2 = petscii:'p'
|
||||
ubyte @shared char1 = 'd'
|
||||
ubyte @shared char2 = petscii:'p'
|
||||
|
||||
sub start() {
|
||||
}
|
||||
|
||||
@@ -570,4 +570,32 @@ main {
|
||||
}"""
|
||||
compileText(C64Target(), optimize=false, src, outputDir) shouldNotBe null
|
||||
}
|
||||
|
||||
test("empty enum declaration should give error") {
|
||||
val src="""
|
||||
main {
|
||||
enum Color {
|
||||
}
|
||||
sub start() { }
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false)
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "enumeration must contain at least one member"
|
||||
}
|
||||
|
||||
test("empty struct declaration should give error") {
|
||||
val src="""
|
||||
main {
|
||||
struct Node {
|
||||
}
|
||||
sub start() { }
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false)
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "struct must contain at least one field"
|
||||
}
|
||||
})
|
||||
@@ -13,37 +13,72 @@ import prog8tests.helpers.compileText
|
||||
class TestPointerConstPromotion : FunSpec({
|
||||
val outputDir = tempdir().toPath()
|
||||
|
||||
test("pointer constant promotion scenarios") {
|
||||
test("zp pointer variable with a dereference is NOT promoted to const") {
|
||||
val src = """
|
||||
%zeropage basicsafe
|
||||
%option no_sysinit
|
||||
main {
|
||||
sub start() {
|
||||
^^uword ptr1 = 4444 ; size 2 > 1, no promotion
|
||||
^^ubyte ptr2 = 5555 ; size 1, promotion
|
||||
uword mem1 = memory("mem1", 10, 0) ; not a pointer, promotion
|
||||
^^uword mem1b = memory("mem2", 10, 0) ; size 2 > 1, no promotion
|
||||
^^ubyte mem3 = memory("mem3", 10, 0) ; size 1, promotion
|
||||
|
||||
cx16.r0 = ptr1
|
||||
cx16.r1 = ptr2 as uword
|
||||
cx16.r2 = mem1
|
||||
cx16.r3 = mem1b
|
||||
cx16.r4 = mem3 as uword
|
||||
^^ubyte @zp ptr = 9999
|
||||
ubyte @shared a = ptr^^
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(VMTarget(), true, src, outputDir)!!
|
||||
val mainSub = result.compilerAst.entrypoint
|
||||
val decls = mainSub.statements.filterIsInstance<VarDecl>().associateBy { it.name }
|
||||
|
||||
decls["ptr1"]?.type shouldBe VarDeclType.VAR
|
||||
decls["ptr2"]?.type shouldBe VarDeclType.CONST
|
||||
decls["mem1"]?.type shouldBe VarDeclType.CONST
|
||||
decls["mem1b"]?.type shouldBe VarDeclType.VAR
|
||||
decls["mem3"]?.type shouldBe VarDeclType.CONST
|
||||
val result = compileText(VMTarget(), true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
|
||||
test("nonzp pointer variable with a dereference is promoted to const") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte @nozp ptr = 9999
|
||||
ubyte @shared a = ptr^^
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(VMTarget(), true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.CONST
|
||||
}
|
||||
|
||||
test("pointer variable with memory is not promoted to const") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = memory("slab", 10, 1)
|
||||
ubyte @shared a = ptr^^
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(VMTarget(), true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
|
||||
test("pointer variable with 3 dereferences is NOT promoted to const") {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
^^ubyte ptr = 9999
|
||||
ubyte @shared a = ptr^^
|
||||
ubyte @shared b = ptr^^
|
||||
ubyte @shared c = ptr^^
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val result = compileText(VMTarget(), true, src, outputDir)
|
||||
val mainBlock = result!!.compilerAst.allBlocks.first { it.name == "main" }
|
||||
val startSub = mainBlock.subScope("start")!!
|
||||
val ptrVar = startSub.lookup(listOf("ptr"))!!
|
||||
(ptrVar as VarDecl).type shouldBe VarDeclType.VAR
|
||||
}
|
||||
|
||||
test("explicit const pointer to >1 byte type gives error") {
|
||||
val src = """
|
||||
main {
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
package prog8tests.ast
|
||||
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.kotest.matchers.string.shouldContain
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import prog8tests.helpers.compileText
|
||||
|
||||
class TestPrivateAccess : FunSpec({
|
||||
val outputDir = tempdir().toPath()
|
||||
|
||||
test("cannot access private variable from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private ubyte x = 10
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
ubyte v = M.x
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "cannot access private variable 'M.x' from outside its block"
|
||||
}
|
||||
|
||||
test("cannot access private subroutine from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private sub foo() {}
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
M.foo()
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "cannot access private subroutine 'M.foo' from outside its block"
|
||||
}
|
||||
|
||||
test("cannot access private struct from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private struct S { ubyte f }
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
ubyte v = sizeof(M.S)
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.any { it.contains("cannot access private struct 'M.S' from outside its block") } shouldBe true
|
||||
}
|
||||
|
||||
test("cannot access private enum from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private enum E { A, B }
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
ubyte v = M.E::A
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
// Enum members are reported as private variable 'M.E::A' because they're converted to constant variables
|
||||
// that inherit the private status of the enum.
|
||||
errors.errors.any { it.contains("cannot access private variable 'M.E::A' from outside its block") } shouldBe true
|
||||
}
|
||||
|
||||
test("cannot access private asmsub from outside its block") {
|
||||
val text = """
|
||||
M {
|
||||
private asmsub foo() {
|
||||
%asm {{
|
||||
rts
|
||||
}}
|
||||
}
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
M.foo()
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "cannot access private subroutine 'M.foo' from outside its block"
|
||||
}
|
||||
|
||||
test("cannot access private extsub from outside its block") {
|
||||
val text = $$"""
|
||||
M {
|
||||
private extsub $c000 = foo()
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
M.foo()
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors) shouldBe null
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "cannot access private subroutine 'M.foo' from outside its block"
|
||||
}
|
||||
|
||||
test("can access private elements from within the same block") {
|
||||
val text = """
|
||||
other {
|
||||
private ubyte x = 10
|
||||
private sub foo() {}
|
||||
private asmsub bar_asm() {
|
||||
rts
|
||||
}
|
||||
private struct S { ubyte f }
|
||||
|
||||
sub bar() {
|
||||
x = 20
|
||||
foo()
|
||||
bar_asm()
|
||||
S* myS = 0
|
||||
}
|
||||
}
|
||||
main {
|
||||
sub start() {
|
||||
other.bar()
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val errors = ErrorReporterForTests(keepMessagesAfterReporting = true)
|
||||
compileText(VMTarget(), true, text, outputDir, errors = errors, writeAssembly = false)
|
||||
errors.errors.size shouldBe 0
|
||||
}
|
||||
})
|
||||
@@ -918,9 +918,9 @@ class TestProg8Parser: FunSpec( {
|
||||
main {
|
||||
sub start() {
|
||||
str string = "\x00\xff\u0041"
|
||||
ubyte zero = '\x00'
|
||||
ubyte ff = '\xff'
|
||||
ubyte letter = '\u0041'
|
||||
ubyte @shared zero = '\x00'
|
||||
ubyte @shared ff = '\xff'
|
||||
ubyte @shared letter = '\u0041'
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -997,10 +997,10 @@ class TestProg8Parser: FunSpec( {
|
||||
sub start() {
|
||||
ubyte @zp @shared @requirezp var1
|
||||
ubyte @shared @zp var2
|
||||
ubyte @zp var3
|
||||
ubyte @zp @shared var3
|
||||
ubyte @shared var4
|
||||
ubyte @requirezp var5
|
||||
ubyte var6
|
||||
ubyte @requirezp @shared var5
|
||||
ubyte @shared var6
|
||||
}
|
||||
}
|
||||
"""
|
||||
@@ -1014,16 +1014,16 @@ class TestProg8Parser: FunSpec( {
|
||||
var2.sharedWithAsm shouldBe true
|
||||
var2.zeropage shouldBe ZeropageWish.PREFER_ZEROPAGE
|
||||
val var3 = stmt[4] as VarDecl
|
||||
var3.sharedWithAsm shouldBe false
|
||||
var3.sharedWithAsm shouldBe true
|
||||
var3.zeropage shouldBe ZeropageWish.PREFER_ZEROPAGE
|
||||
val var4 = stmt[6] as VarDecl
|
||||
var4.sharedWithAsm shouldBe true
|
||||
var4.zeropage shouldBe ZeropageWish.DONTCARE
|
||||
val var5 = stmt[8] as VarDecl
|
||||
var5.sharedWithAsm shouldBe false
|
||||
var5.sharedWithAsm shouldBe true
|
||||
var5.zeropage shouldBe ZeropageWish.REQUIRE_ZEROPAGE
|
||||
val var6 = stmt[10] as VarDecl
|
||||
var6.sharedWithAsm shouldBe false
|
||||
var6.sharedWithAsm shouldBe true
|
||||
var6.zeropage shouldBe ZeropageWish.DONTCARE
|
||||
}
|
||||
|
||||
@@ -1089,10 +1089,10 @@ main {
|
||||
%option enable_floats
|
||||
main {
|
||||
sub start() {
|
||||
uword w1 = 000_1234_5__
|
||||
uword w2 = ${'$'}ff_ee
|
||||
uword w3 = %11_0000_111111__0000
|
||||
float fl = 3_000_001.141_592_654
|
||||
uword @shared w1 = 000_1234_5__
|
||||
uword @shared w2 = ${'$'}ff_ee
|
||||
uword @shared w3 = %11_0000_111111__0000
|
||||
float @shared fl = 3_000_001.141_592_654
|
||||
}
|
||||
}"""
|
||||
val result = compileText(VMTarget(), false, src, outputDir, writeAssembly = false)!!
|
||||
|
||||
@@ -14,6 +14,7 @@ import prog8.code.core.DataType
|
||||
import prog8.code.source.SourceCode
|
||||
import prog8.code.target.C64Target
|
||||
import prog8.code.target.Cx16Target
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.parser.Prog8Parser.parseModule
|
||||
import prog8tests.helpers.ErrorReporterForTests
|
||||
import prog8tests.helpers.compileText
|
||||
@@ -279,4 +280,24 @@ main {
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "doesn't end with a return"
|
||||
}
|
||||
|
||||
test("asmsub body restriction") {
|
||||
val src="""
|
||||
main {
|
||||
asmsub foo() {
|
||||
ubyte x = 1
|
||||
}
|
||||
asmsub ok1() {
|
||||
%asm {{ nop }}
|
||||
}
|
||||
asmsub ok2() {
|
||||
%asm {{ nop }}
|
||||
%asm {{ nop }}
|
||||
}
|
||||
}"""
|
||||
val errors = ErrorReporterForTests()
|
||||
compileText(VMTarget(), false, src, outputDir, errors, false)
|
||||
errors.errors.size shouldBe 1
|
||||
errors.errors[0] shouldContain "asmsub can only contain inline assembly (%asm)"
|
||||
}
|
||||
})
|
||||
@@ -458,11 +458,11 @@ main {
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
ubyte w
|
||||
ubyte @shared w
|
||||
|
||||
for w in 0 to 20 {
|
||||
ubyte @zp x,y,z=13
|
||||
ubyte q,r,s
|
||||
ubyte @shared @zp x,y,z=13
|
||||
ubyte @shared q,r,s
|
||||
x++
|
||||
y++
|
||||
z++
|
||||
@@ -471,28 +471,6 @@ main {
|
||||
}"""
|
||||
val result = compileText(VMTarget(), optimize = false, src, outputDir, writeAssembly = false)!!
|
||||
val st = result.compilerAst.entrypoint.statements
|
||||
/*
|
||||
sub start () {
|
||||
ubyte s
|
||||
s = 0
|
||||
ubyte r
|
||||
r = 0
|
||||
ubyte q
|
||||
q = 0
|
||||
ubyte @zp z
|
||||
ubyte @zp y
|
||||
ubyte @zp x
|
||||
ubyte w
|
||||
for w in 0 to 20 step 1 {
|
||||
z = 13
|
||||
y = 13
|
||||
x = 13
|
||||
x++
|
||||
y++
|
||||
z++
|
||||
}
|
||||
}
|
||||
*/
|
||||
val vars = st.filterIsInstance<VarDecl>()
|
||||
vars.size shouldBe 7
|
||||
vars.all { it.names.size<=1 } shouldBe true
|
||||
@@ -724,8 +702,8 @@ main {
|
||||
val src="""
|
||||
main {
|
||||
sub start() {
|
||||
ubyte shift = 10
|
||||
uword value = 1<<shift
|
||||
ubyte @shared shift = 10
|
||||
uword @shared value = 1<<shift
|
||||
value++
|
||||
value = 1<<shift
|
||||
value++
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package prog8tests.codegeneration
|
||||
|
||||
import io.kotest.assertions.fail
|
||||
import io.kotest.core.spec.style.FunSpec
|
||||
import io.kotest.engine.spec.tempdir
|
||||
import io.kotest.matchers.shouldBe
|
||||
@@ -24,7 +25,7 @@ class Test2DArrays: FunSpec({
|
||||
ubyte[3][4] m
|
||||
m[0][0] = 1
|
||||
m[2][3] = 12
|
||||
ubyte v = m[0][0]
|
||||
ubyte @shared v = m[0][0]
|
||||
}
|
||||
}""",
|
||||
// Variable indexing
|
||||
@@ -32,10 +33,10 @@ class Test2DArrays: FunSpec({
|
||||
main {
|
||||
sub start() {
|
||||
ubyte[3][4] m
|
||||
ubyte r = 1
|
||||
ubyte c = 2
|
||||
ubyte @shared r = 1
|
||||
ubyte @shared c = 2
|
||||
m[r][c] = 42
|
||||
ubyte v = m[r][c]
|
||||
ubyte @shared v = m[r][c]
|
||||
}
|
||||
}""",
|
||||
// Used in expressions
|
||||
@@ -45,7 +46,7 @@ class Test2DArrays: FunSpec({
|
||||
ubyte[2][3] m
|
||||
m[0][0] = 10
|
||||
m[1][1] = 15
|
||||
ubyte s = m[0][0] + m[1][1]
|
||||
ubyte @shared s = m[0][0] + m[1][1]
|
||||
}
|
||||
}""",
|
||||
// As function argument
|
||||
@@ -73,8 +74,8 @@ class Test2DArrays: FunSpec({
|
||||
main {
|
||||
ubyte[4][4] m
|
||||
sub start() {
|
||||
ubyte r = 0
|
||||
ubyte c = 0
|
||||
ubyte @shared r = 0
|
||||
ubyte @shared c = 0
|
||||
for r in 0 to 3 {
|
||||
for c in 0 to 3 {
|
||||
m[r][c] = r * c
|
||||
@@ -87,13 +88,14 @@ class Test2DArrays: FunSpec({
|
||||
main {
|
||||
sub start() {
|
||||
ubyte[3][4] m
|
||||
uword addr = &m
|
||||
uword @shared addr = &m
|
||||
}
|
||||
}"""
|
||||
)
|
||||
for(src in sources) {
|
||||
compileText(VMTarget(), false, src, outputDir) shouldNotBe null
|
||||
compileText(C64Target(), false, src, outputDir) shouldNotBe null
|
||||
val result = compileText(VMTarget(), false, src, outputDir) ?: fail("Compilation failed for source:\n$src")
|
||||
val resultc64 =
|
||||
compileText(C64Target(), false, src, outputDir) ?: fail("Compilation failed for source:\n$src")
|
||||
}
|
||||
// Also verify optimized compilation
|
||||
compileText(VMTarget(), true, sources[0], outputDir) shouldNotBe null
|
||||
|
||||
@@ -14,8 +14,8 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long a = 1000
|
||||
long b = 2000
|
||||
long @shared a = 1000
|
||||
long @shared b = 2000
|
||||
a += b
|
||||
}
|
||||
}
|
||||
@@ -30,8 +30,8 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
long a = 2000
|
||||
long b = 1000
|
||||
long @shared a = 2000
|
||||
long @shared b = 1000
|
||||
a -= b
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,7 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
word w = -1
|
||||
word @shared w = -1
|
||||
if w <= 0 {
|
||||
cx16.r0 = 1
|
||||
}
|
||||
@@ -64,7 +64,7 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
word w = 1
|
||||
word @shared w = 1
|
||||
if w > 0 {
|
||||
cx16.r0 = 1
|
||||
}
|
||||
@@ -82,7 +82,7 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
uword uw = 0
|
||||
uword @shared uw = 0
|
||||
if uw <= 0 {
|
||||
cx16.r0 = 1
|
||||
}
|
||||
@@ -100,7 +100,7 @@ class TestComparisonIssues : FunSpec({
|
||||
val text = """
|
||||
main {
|
||||
sub start() {
|
||||
word w = 1
|
||||
word @shared w = 1
|
||||
if w <= 0 {
|
||||
cx16.r0 = 1
|
||||
} else {
|
||||
|
||||
@@ -115,10 +115,26 @@ class TestExecution6502 : FunSpec({
|
||||
}
|
||||
""".trimIndent()
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
val machine = compileResult!!.simulate()
|
||||
val machine = compileResult!!.simulate(1000)
|
||||
machine.cpu.totalCycles shouldBeLessThan 200
|
||||
}
|
||||
|
||||
test("top level RTS exits simulator") {
|
||||
val src = $$"""
|
||||
%option no_sysinit
|
||||
%launcher none
|
||||
%address $1000
|
||||
main {
|
||||
sub start() {
|
||||
; do nothing just RTS
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
val machine = compileResult!!.simulate(1000)
|
||||
machine.cpu.totalCycles shouldBeLessThan 250
|
||||
}
|
||||
|
||||
test("reset register") {
|
||||
val src = $$"""
|
||||
%option no_sysinit
|
||||
@@ -287,4 +303,42 @@ class TestExecution6502 : FunSpec({
|
||||
compileResult!!.simulate(maxCycles = 100)
|
||||
}
|
||||
}
|
||||
|
||||
test("div_longs positive 1234567890 / 88888 = 13889") {
|
||||
val src = $$"""
|
||||
%option no_sysinit
|
||||
%launcher none
|
||||
%address $1000
|
||||
|
||||
main {
|
||||
&ubyte poweroff = $f203
|
||||
sub start() {
|
||||
long @shared v = 1234567890
|
||||
v /= 88888
|
||||
%asm {{
|
||||
lda p8v_v
|
||||
sta $2000
|
||||
lda p8v_v+1
|
||||
sta $2001
|
||||
lda p8v_v+2
|
||||
sta $2002
|
||||
lda p8v_v+3
|
||||
sta $2003
|
||||
}}
|
||||
poweroff = 1
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
val compileResult = compileText(Cx16Target(), false, src, outputDir)
|
||||
val machine = compileResult!!.simulate(maxCycles = 30000)
|
||||
val b0 = machine.ram[0x2000].toInt() and 0xff
|
||||
val b1 = machine.ram[0x2001].toInt() and 0xff
|
||||
val b2 = machine.ram[0x2002].toInt() and 0xff
|
||||
val b3 = machine.ram[0x2003].toInt() and 0xff
|
||||
println("div_longs result bytes: $b0 $b1 $b2 $b3 (hex: ${b0.toString(16)} ${b1.toString(16)} ${b2.toString(16)} ${b3.toString(16)})")
|
||||
machine.assertMemory(0x2000, 0x41)
|
||||
machine.assertMemory(0x2001, 0x36)
|
||||
machine.assertMemory(0x2002, 0x00)
|
||||
machine.assertMemory(0x2003, 0x00)
|
||||
}
|
||||
})
|
||||
@@ -210,8 +210,8 @@ test("if-expression bitwise AND optimization (BIT instruction)") {
|
||||
main {
|
||||
word[10] @shared words
|
||||
sub start() {
|
||||
ubyte i = 5
|
||||
ubyte result = if words[i] < 0 then 1 else 0
|
||||
ubyte @shared i = 5
|
||||
ubyte @shared result = if words[i] < 0 then 1 else 0
|
||||
cx16.r1L = result
|
||||
}
|
||||
}
|
||||
@@ -228,8 +228,8 @@ test("if-expression bitwise AND optimization (BIT instruction)") {
|
||||
main {
|
||||
long[10] @shared longs
|
||||
sub start() {
|
||||
ubyte i = 2
|
||||
ubyte result = if longs[i] > 0 then 1 else 0
|
||||
ubyte @shared i = 2
|
||||
ubyte @shared result = if longs[i] > 0 then 1 else 0
|
||||
cx16.r1L = result
|
||||
}
|
||||
}
|
||||
@@ -279,8 +279,8 @@ test("if-expression bitwise AND optimization (BIT instruction)") {
|
||||
main {
|
||||
uword[10] @shared words
|
||||
sub start() {
|
||||
ubyte i = 5
|
||||
ubyte result = if words[i] == 0 then 1 else 0
|
||||
ubyte @shared i = 5
|
||||
ubyte @shared result = if words[i] == 0 then 1 else 0
|
||||
cx16.r1L = result
|
||||
}
|
||||
}
|
||||
@@ -295,8 +295,8 @@ test("if-expression bitwise AND optimization (BIT instruction)") {
|
||||
main {
|
||||
word[10] @shared words
|
||||
sub start() {
|
||||
ubyte i = 2
|
||||
ubyte result = if words[i] != 0 then 1 else 0
|
||||
ubyte @shared i = 2
|
||||
ubyte @shared result = if words[i] != 0 then 1 else 0
|
||||
cx16.r1L = result
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,9 +97,9 @@ main {
|
||||
main {
|
||||
sub start() {
|
||||
uword[3] arr
|
||||
ubyte i = 42
|
||||
uword ww = peekw(arr[i])
|
||||
ubyte xx = peek(arr[i])
|
||||
ubyte @shared i = 42
|
||||
uword @shared ww = peekw(arr[i])
|
||||
ubyte @shared xx = peek(arr[i])
|
||||
xx = @(arr[i])
|
||||
|
||||
@(arr[i]) = 42
|
||||
@@ -114,7 +114,7 @@ main {
|
||||
val text="""
|
||||
main {
|
||||
sub start() {
|
||||
uword factor1
|
||||
uword @shared factor1
|
||||
ubyte[3] bytearray
|
||||
uword[3] wordarray
|
||||
@(factor1) = bytearray[0]
|
||||
@@ -308,10 +308,10 @@ main
|
||||
{
|
||||
sub start()
|
||||
{
|
||||
uword uw = 54321
|
||||
ubyte ub = 123
|
||||
word sw = -12345
|
||||
byte sb = -123
|
||||
uword @shared uw = 54321
|
||||
ubyte @shared ub = 123
|
||||
word @shared sw = -12345
|
||||
byte @shared sb = -123
|
||||
|
||||
func_uw(~ub as uword)
|
||||
func_ub(~uw as ubyte)
|
||||
|
||||
@@ -11,7 +11,7 @@ fun String.shouldContainInOrder(vararg substrings: String) {
|
||||
for (substring in substrings) {
|
||||
val index = this.indexOf(substring, currentSearchIndex)
|
||||
if (index == -1) {
|
||||
fail("Could not find '$substring' after index $currentSearchIndex in string:\n$this")
|
||||
fail("Could not find '$substring' after index $currentSearchIndex")
|
||||
}
|
||||
currentSearchIndex = index + substring.length
|
||||
}
|
||||
|
||||
@@ -375,7 +375,7 @@ main {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
bool flag = true
|
||||
bool @shared flag = true
|
||||
cx16.r0L = if flag then 1 else 0
|
||||
}
|
||||
}"""
|
||||
@@ -398,7 +398,7 @@ main {
|
||||
val src = """
|
||||
main {
|
||||
sub start() {
|
||||
bool flag = true
|
||||
bool @shared flag = true
|
||||
if flag {
|
||||
cx16.r0L = 1
|
||||
} else {
|
||||
|
||||
@@ -127,6 +127,9 @@ class AstToSourceTextConverter(val output: (text: String) -> Unit, val program:
|
||||
if(decl.origin==VarDeclOrigin.SUBROUTINEPARAM)
|
||||
return
|
||||
|
||||
if(decl.isPrivate)
|
||||
output("private ")
|
||||
|
||||
when(decl.type) {
|
||||
VarDeclType.VAR -> {}
|
||||
VarDeclType.CONST -> output("const ")
|
||||
@@ -187,6 +190,8 @@ class AstToSourceTextConverter(val output: (text: String) -> Unit, val program:
|
||||
}
|
||||
|
||||
override fun visit(struct: StructDecl) {
|
||||
if(struct.isPrivate)
|
||||
output("private ")
|
||||
outputln("struct ${struct.name} {")
|
||||
for(member in struct.fields) {
|
||||
outputlni( " ${member.first} ${member.second}")
|
||||
@@ -194,9 +199,25 @@ class AstToSourceTextConverter(val output: (text: String) -> Unit, val program:
|
||||
outputlni("}")
|
||||
}
|
||||
|
||||
override fun visit(enum: Enumeration) {
|
||||
if(enum.isPrivate)
|
||||
output("private ")
|
||||
output("enum ${enum.name} { ")
|
||||
for(member in enum.members) {
|
||||
output(member.first)
|
||||
if(member.second!=null)
|
||||
output(" = ${member.second}")
|
||||
if(member !== enum.members.last())
|
||||
output(", ")
|
||||
}
|
||||
outputln(" }")
|
||||
}
|
||||
|
||||
override fun visit(subroutine: Subroutine) {
|
||||
output("\n")
|
||||
outputi("")
|
||||
if(subroutine.isPrivate)
|
||||
output("private ")
|
||||
if(subroutine.inline)
|
||||
output("inline ")
|
||||
if(subroutine.isAsmSubroutine) {
|
||||
@@ -569,6 +590,8 @@ class AstToSourceTextConverter(val output: (text: String) -> Unit, val program:
|
||||
}
|
||||
|
||||
override fun visit(alias: Alias) {
|
||||
if(alias.isPrivate)
|
||||
output("private ")
|
||||
output("alias ${alias.alias} = ${alias.target.nameInSource.joinToString(".")}")
|
||||
}
|
||||
|
||||
|
||||
@@ -36,6 +36,9 @@ interface IFunctionCall {
|
||||
val IFunctionCall.isMemoryCall: Boolean
|
||||
get() = target.nameInSource == listOf("memory")
|
||||
|
||||
val IFunctionCall.isMemoryRefCall: Boolean
|
||||
get() = target.nameInSource == listOf("memory__ref")
|
||||
|
||||
interface IStatementContainer {
|
||||
val statements: MutableList<Statement>
|
||||
|
||||
|
||||
@@ -128,7 +128,7 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
|
||||
override fun visitAlias(ctx: AliasContext): Alias {
|
||||
val identifier = getname(ctx.identifier())
|
||||
val target = ctx.scoped_identifier().accept(this) as IdentifierReference
|
||||
return Alias(identifier, target, ctx.toPosition())
|
||||
return Alias(identifier, target, ctx.PRIVATE() != null, ctx.toPosition())
|
||||
}
|
||||
|
||||
override fun visitDefer(ctx: DeferContext): Defer {
|
||||
@@ -652,7 +652,7 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
|
||||
val address = Subroutine.Address(constbank, varbank, addr)
|
||||
return Subroutine(subdecl.name, subdecl.parameters, subdecl.returntypes,
|
||||
subdecl.asmParameterRegisters, subdecl.asmReturnvaluesRegisters,
|
||||
subdecl.asmClobbers, address, true, inline = false, isPrivate = false, statements = mutableListOf(), position = ctx.toPosition()
|
||||
subdecl.asmClobbers, address, true, inline = false, isPrivate = ctx.PRIVATE() != null, statements = mutableListOf(), position = ctx.toPosition()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -795,7 +795,7 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
|
||||
val name = getname(ctx.identifier())
|
||||
val fields: List<Pair<DataType, List<String>>> = ctx.structfielddecl().map { getStructField(it) }
|
||||
val flattened = fields.flatMap { (dt, names) -> names.map { dt to it}}
|
||||
return StructDecl(name, flattened.toTypedArray(), ctx.toPosition())
|
||||
return StructDecl(name, flattened.toTypedArray(), ctx.PRIVATE() != null, ctx.toPosition())
|
||||
}
|
||||
|
||||
private fun getStructField(ctx: StructfielddeclContext): Pair<DataType, List<String>> {
|
||||
@@ -819,7 +819,7 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
|
||||
val members = members1.map {
|
||||
it.first to it.second?.number?.toInt()
|
||||
}.toTypedArray()
|
||||
return Enumeration(name, largestType, members, ctx.toPosition())
|
||||
return Enumeration(name, largestType, members, ctx.PRIVATE() != null, ctx.toPosition())
|
||||
}
|
||||
|
||||
|
||||
@@ -985,7 +985,7 @@ class Antlr2KotlinVisitor(val source: SourceCode): AbstractParseTreeVisitor<Node
|
||||
val pointer = pointerDatatypeFor(dtctx.pointertype())
|
||||
if(pointer!=null)
|
||||
return pointer
|
||||
val struct = dtctx.structtype.identifier().map { dtctx.text }
|
||||
val struct = dtctx.structtype.identifier().map { it.text }
|
||||
return DataType.structInstanceFromAntlr(struct)
|
||||
}
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ data class DirectiveArg(val string: String?, val int: UInt?, override val positi
|
||||
override fun referencesIdentifier(nameInSource: List<String>): Boolean = false
|
||||
}
|
||||
|
||||
data class Alias(val alias: String, val target: IdentifierReference, override val position: Position) : Statement() {
|
||||
data class Alias(val alias: String, val target: IdentifierReference, val isPrivate: Boolean = false, override val position: Position) : Statement() {
|
||||
override lateinit var parent: Node
|
||||
|
||||
override fun linkParents(parent: Node) {
|
||||
@@ -161,7 +161,7 @@ data class Alias(val alias: String, val target: IdentifierReference, override va
|
||||
target.parent = this
|
||||
}
|
||||
|
||||
override fun copy(): Statement = Alias(alias, target.copy(), position)
|
||||
override fun copy(): Statement = Alias(alias, target.copy(), isPrivate, position)
|
||||
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
|
||||
override fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent)
|
||||
override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here")
|
||||
@@ -235,7 +235,7 @@ class Continue(override val position: Position) : Statement() {
|
||||
}
|
||||
|
||||
|
||||
class MemorySlabReservation(
|
||||
data class MemorySlabReservation(
|
||||
val slabName: String,
|
||||
val size: UInt,
|
||||
val align: UInt,
|
||||
@@ -512,7 +512,7 @@ class VarDecl(
|
||||
}
|
||||
}
|
||||
|
||||
class StructDecl(override val name: String, val fields: Array<Pair<DataType, String>>, override val position: Position) : Statement(), INamedStatement, ISubType {
|
||||
class StructDecl(override val name: String, val fields: Array<Pair<DataType, String>>, val isPrivate: Boolean, override val position: Position) : Statement(), INamedStatement, ISubType {
|
||||
override lateinit var parent: Node
|
||||
|
||||
override fun linkParents(parent: Node) {
|
||||
@@ -521,7 +521,7 @@ class StructDecl(override val name: String, val fields: Array<Pair<DataType, Str
|
||||
|
||||
override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here")
|
||||
override fun referencesIdentifier(nameInSource: List<String>) = false
|
||||
override fun copy() = StructDecl(name, fields.clone(), position)
|
||||
override fun copy() = StructDecl(name, fields.clone(), isPrivate, position)
|
||||
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
|
||||
override fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent)
|
||||
override fun memsize(sizer: IMemSizer): Int = fields.sumOf { sizer.memorySize(it.first, 1) }
|
||||
@@ -562,7 +562,7 @@ class StructFieldRef(val pointer: IdentifierReference, val struct: StructDecl, v
|
||||
|
||||
}
|
||||
|
||||
class Enumeration(override val name: String, val type: BaseDataType, val members: Array<Pair<String, Int?>>, override val position: Position) : Statement(), INamedStatement {
|
||||
class Enumeration(override val name: String, val type: BaseDataType, val members: Array<Pair<String, Int?>>, val isPrivate: Boolean, override val position: Position) : Statement(), INamedStatement {
|
||||
override lateinit var parent: Node
|
||||
|
||||
override fun linkParents(parent: Node) {
|
||||
@@ -571,7 +571,7 @@ class Enumeration(override val name: String, val type: BaseDataType, val members
|
||||
|
||||
override fun replaceChildNode(node: Node, replacement: Node) = throw FatalAstException("can't replace here")
|
||||
override fun referencesIdentifier(nameInSource: List<String>) = false
|
||||
override fun copy(): Enumeration = Enumeration(name, type, members.toList().toTypedArray(), position)
|
||||
override fun copy(): Enumeration = Enumeration(name, type, members.toList().toTypedArray(), isPrivate, position)
|
||||
override fun accept(visitor: IAstVisitor) = visitor.visit(this)
|
||||
override fun accept(visitor: AstWalker, parent: Node) = visitor.visit(this, parent)
|
||||
}
|
||||
|
||||
@@ -116,9 +116,10 @@ class CallGraph(private val program: Program) : IAstVisitor {
|
||||
|
||||
override fun visit(identifier: IdentifierReference) {
|
||||
val target = identifier.targetStatement(program.builtinFunctions)
|
||||
val definingSub = identifier.definingSubroutine
|
||||
if(target!=null) {
|
||||
allIdentifiersAndTargets.add(identifier to target)
|
||||
if(target is Subroutine)
|
||||
if(target is Subroutine && target !== definingSub)
|
||||
notCalledButReferenced += target
|
||||
}
|
||||
|
||||
@@ -128,9 +129,10 @@ class CallGraph(private val program: Program) : IAstVisitor {
|
||||
while(name.size>1) {
|
||||
name.removeLast()
|
||||
val scopeTarget = scope.lookup(name)
|
||||
if(scopeTarget is Subroutine)
|
||||
notCalledButReferenced += scopeTarget
|
||||
else if(scopeTarget is VarDecl) {
|
||||
if(scopeTarget is Subroutine) {
|
||||
if (scopeTarget !== definingSub)
|
||||
notCalledButReferenced += scopeTarget
|
||||
} else if(scopeTarget is VarDecl) {
|
||||
allIdentifiersAndTargets.add(identifier to scopeTarget)
|
||||
break
|
||||
} else if(scopeTarget is StructFieldRef) {
|
||||
@@ -164,13 +166,15 @@ class CallGraph(private val program: Program) : IAstVisitor {
|
||||
|
||||
override fun visit(deref: PtrDereference) {
|
||||
val chain = deref.chain.toMutableList()
|
||||
val definingSub = deref.definingSubroutine
|
||||
while(chain.isNotEmpty()) {
|
||||
val variable = deref.definingScope.lookup(chain)
|
||||
if(variable is VarDecl) {
|
||||
allIdentifiersAndTargets.add(deref to variable)
|
||||
}
|
||||
else if(variable is Subroutine) {
|
||||
notCalledButReferenced += variable
|
||||
if (variable !== definingSub)
|
||||
notCalledButReferenced += variable
|
||||
}
|
||||
chain.removeLastOrNull()
|
||||
}
|
||||
@@ -179,13 +183,15 @@ class CallGraph(private val program: Program) : IAstVisitor {
|
||||
|
||||
override fun visit(deref: ArrayIndexedPtrDereference) {
|
||||
val chain = deref.chain.map { it.first }.toMutableList()
|
||||
val definingSub = deref.definingSubroutine
|
||||
while(chain.isNotEmpty()) {
|
||||
val variable = deref.definingScope.lookup(chain)
|
||||
if(variable is VarDecl) {
|
||||
allIdentifiersAndTargets.add(deref to variable)
|
||||
}
|
||||
else if(variable is Subroutine) {
|
||||
notCalledButReferenced += variable
|
||||
if (variable !== definingSub)
|
||||
notCalledButReferenced += variable
|
||||
}
|
||||
chain.removeLastOrNull()
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
%import cx16logo
|
||||
%import diskio
|
||||
;;%import floats
|
||||
%import lineclip
|
||||
%import math
|
||||
%import petgfx
|
||||
%import prog8_lib
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
%import diskio
|
||||
%import floats
|
||||
%import graphics
|
||||
%import lineclip
|
||||
%import math
|
||||
%import petgfx
|
||||
%import prog8_lib
|
||||
|
||||
@@ -14,12 +14,14 @@
|
||||
%import gfx_hires
|
||||
%import gfx_lores
|
||||
%import graphics
|
||||
%import lineclip
|
||||
%import math
|
||||
%import monogfx
|
||||
%import palette
|
||||
%import prog8_lib
|
||||
%import psg
|
||||
%import psg2
|
||||
%import serial
|
||||
%import sorting
|
||||
%import sprites
|
||||
%import strings
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
%import cx16logo
|
||||
%import diskio
|
||||
%import floats
|
||||
%import lineclip
|
||||
%import math
|
||||
%import petsnd
|
||||
%import petgfx
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
%import diskio
|
||||
%import emudbg
|
||||
%import floats
|
||||
%import lineclip
|
||||
%import math
|
||||
%import monogfx
|
||||
%import prog8_lib
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
Prog8 compiler v12.2-SNAPSHOT by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit c784fd03 in branch master
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-c128.p8
|
||||
@@ -274,6 +274,16 @@ diskio {
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: lineclip
|
||||
-----------------------------
|
||||
|
||||
lineclip {
|
||||
clip (word x1, word y1, word x2, word y2) -> bool, word, word, word, word
|
||||
inside (word x, word y) -> bool
|
||||
set_cliprect (word x1, word y1, word x2, word y2)
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: math
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
Prog8 compiler v12.2-SNAPSHOT by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit c784fd03 in branch master
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-c64.p8
|
||||
@@ -395,6 +395,16 @@ graphics {
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: lineclip
|
||||
-----------------------------
|
||||
|
||||
lineclip {
|
||||
clip (word x1, word y1, word x2, word y2) -> bool, word, word, word, word
|
||||
inside (word x, word y) -> bool
|
||||
set_cliprect (word x1, word y1, word x2, word y2)
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: math
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
Prog8 compiler v12.2-SNAPSHOT by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit c784fd03 in branch master
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-cx16.p8
|
||||
@@ -588,6 +588,16 @@ graphics {
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: lineclip
|
||||
-----------------------------
|
||||
|
||||
lineclip {
|
||||
clip (word x1, word y1, word x2, word y2) -> bool, word, word, word, word
|
||||
inside (word x, word y) -> bool
|
||||
set_cliprect (word x1, word y1, word x2, word y2)
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: math
|
||||
-------------------------
|
||||
|
||||
@@ -774,6 +784,42 @@ psg2 {
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: serial
|
||||
---------------------------
|
||||
|
||||
serial {
|
||||
const ubyte LSR::BI
|
||||
const ubyte LSR::DR
|
||||
const ubyte LSR::FE
|
||||
const ubyte LSR::OE
|
||||
const ubyte LSR::PE
|
||||
const ubyte LSR::RCVR
|
||||
const ubyte LSR::TEMT
|
||||
const ubyte LSR::THRE
|
||||
const ubyte REG_DIVISOR_LATCH_HI
|
||||
const ubyte REG_DIVISOR_LATCH_LOW
|
||||
const ubyte REG_FIFO_CONTROL
|
||||
const ubyte REG_INTERRUPT_ENABLE
|
||||
const ubyte REG_INTERRUPT_IDENT
|
||||
const ubyte REG_LINE_CONTROL
|
||||
const ubyte REG_LINE_STATUS
|
||||
const ubyte REG_MODEM_CONTROL
|
||||
const ubyte REG_MODEM_STATUS
|
||||
const ubyte REG_SCRATCH
|
||||
const ubyte REG_TXRX_BUFFER
|
||||
detect_uarts () -> uword, uword
|
||||
initialize_uart (uword uart_addr)
|
||||
read_until (uword uart_addr, str match, ^^ubyte buffer, uword max_size) -> uword
|
||||
write (uword uart_addr, str data)
|
||||
zi_get_file_chunk (^^ubyte buffer, uword buffer_size, long remaining_file_size) -> uword
|
||||
zi_get_ip_address () -> str
|
||||
zi_initialize (uword uart_addr)
|
||||
zi_reset ()
|
||||
zi_start_get_file (str filename) -> long
|
||||
zi_write_cmd (str command)
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: sorting
|
||||
----------------------------
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
Prog8 compiler v12.2-SNAPSHOT by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit c784fd03 in branch master
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-pet32.p8
|
||||
@@ -335,6 +335,16 @@ floats {
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: lineclip
|
||||
-----------------------------
|
||||
|
||||
lineclip {
|
||||
clip (word x1, word y1, word x2, word y2) -> bool, word, word, word, word
|
||||
inside (word x, word y) -> bool
|
||||
set_cliprect (word x1, word y1, word x2, word y2)
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: math
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
Prog8 compiler v12.2-SNAPSHOT by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit c784fd03 in branch master
|
||||
Prog8 compiler v12.2-BETA2 by Irmen de Jong (irmen@razorvine.net)
|
||||
Prerelease version from git commit 1dbaa6e0 in branch master
|
||||
This software is licensed under the GNU GPL 3.0, see https://www.gnu.org/licenses/gpl.html
|
||||
|
||||
Compiling program import-all-virtual.p8
|
||||
@@ -265,6 +265,16 @@ floats {
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: lineclip
|
||||
-----------------------------
|
||||
|
||||
lineclip {
|
||||
clip (word x1, word y1, word x2, word y2) -> bool, word, word, word, word
|
||||
inside (word x, word y) -> bool
|
||||
set_cliprect (word x1, word y1, word x2, word y2)
|
||||
}
|
||||
|
||||
|
||||
LIBRARY MODULE NAME: math
|
||||
-------------------------
|
||||
|
||||
|
||||
@@ -183,6 +183,16 @@ One or more .p8 module files
|
||||
Add this user-defined symbol directly to the beginning of the generated assembly file.
|
||||
Can be repeated to define multiple symbols.
|
||||
|
||||
``-daemon``
|
||||
Use the prog8c compilation daemon (auto-starts it if not running).
|
||||
This keeps the compiler running as a background server process, which greatly speeds up
|
||||
subsequent compilations by eliminating JVM startup overhead and allowing reuse of cached data.
|
||||
The daemon listens on a Unix domain socket and automatically shuts down after a period of inactivity.
|
||||
Unlike ``-watch``, the daemon does not monitor files for changes: you still invoke ``prog8c -daemon``
|
||||
each time you want to compile. The speedup comes from the warm JVM, not from automatic recompilation.
|
||||
Because each invocation is an individual compiler run, you can compile different source files or
|
||||
pass different options on each call — the daemon does not lock you into a single project or set of flags.
|
||||
|
||||
``-dumpsymbols``
|
||||
print a dump of the variable declarations and subroutine signatures
|
||||
|
||||
@@ -338,6 +348,9 @@ One or more .p8 module files
|
||||
edit prog8 source files with an editor on the X16 itself (such as XVI, XEdit). When saving the source file,
|
||||
the compiler can sit in the background in watch mode, and recompile it for you immediately on save.
|
||||
Then you can directly run the new resulting PRG file in the X16!
|
||||
Unlike ``-daemon``, the watch mode keeps the process in the foreground and automatically
|
||||
recompiles on file changes, whereas ``-daemon`` is a background server that you invoke explicitly
|
||||
each time you want to compile.
|
||||
|
||||
|
||||
Module source code files
|
||||
|
||||
+84
-11
@@ -280,9 +280,14 @@ sizeof (name) ; sizeof(datatype) ; sizeof(&name) ; sizeof(&&name) ; size
|
||||
Note that complicated expressions that you want to swap, may not be implemented yet. To avoid such errors you'll have to just swap them
|
||||
in the old fashioned way, until an optimized code path gets implemented in a future Prog8 version.
|
||||
|
||||
:index:`memory` (name, size, alignment)
|
||||
Returns the address of the first location of a statically "reserved" block of memory of the given size in bytes,
|
||||
with the given name. The name must be a string literal, it cannot be empty or be a variable.
|
||||
:index:`memory` (name, size, alignment) ; memory(name)
|
||||
Returns the address of the first location of a statically "reserved" block of memory with the given name.
|
||||
The 3-argument version reserves a block of the given size in bytes and optional alignment, and returns the adress of it.
|
||||
The 1-argument version acts only as a reference to a block that must be reserved elsewhere in the program.
|
||||
The name must be a string literal, it cannot be empty or be a variable.
|
||||
This routine can be used to "reserve" parts of the memory where a normal byte array variable would
|
||||
not suffice; for instance if you need more than 256 consecutive bytes.
|
||||
The return value is an uword address, and you can use that like a pointer to the memory buffer.
|
||||
The block is *uninitialized memory*; unlike other variables in Prog8 it is *not* set to zero at the start of the program!
|
||||
(if that is required, you can do so yourself using ``memset``).
|
||||
No *dynamic* allocation is done; the block with this name is placed in memory only once!
|
||||
@@ -291,11 +296,8 @@ sizeof (name) ; sizeof(datatype) ; sizeof(&name) ; sizeof(&&name) ; size
|
||||
memory block is aligned on a page boundary, and $2 means word aligned (even addresses).
|
||||
Requesting the address of such a named memory block again later with
|
||||
the same name, will result in the same address as before.
|
||||
When reusing blocks in that way, it is required that the size argument is the same,
|
||||
otherwise you'll get a compilation error.
|
||||
This routine can be used to "reserve" parts of the memory where a normal byte array variable would
|
||||
not suffice; for instance if you need more than 256 consecutive bytes.
|
||||
The return value is an uword address, and you can use that like a pointer to the memory buffer.
|
||||
When reusing blocks in that way (using the 3-argument version), it is required that the size and alignment arguments are identical,
|
||||
otherwise you'll get a compilation error. To avoid duplicating these details it's easier to use the 1-argument version and just refer to the name only.
|
||||
|
||||
:index:`call` (address) -> uword
|
||||
Calls a subroutine given by its memory address. You cannot pass arguments directly,
|
||||
@@ -828,6 +830,28 @@ Read the :source:`graphics source code <compiler/res/prog8lib/c64/graphics.p8>`
|
||||
to see what's in there. (Note: slight variations for different compiler targets)
|
||||
|
||||
|
||||
lineclip
|
||||
^^^^^^^^
|
||||
.. index:: pair: Libraries; lineclip
|
||||
|
||||
Line clipping using the Elite-inspired two-stage algorithm (BBC Micro, 6502).
|
||||
Based on the deep dive by Mark Moxon at https://elite.bbcelite.com/deep_dives/line-clipping.html
|
||||
|
||||
All coordinates are signed words (``word``). The clipping rectangle is set via ``set_cliprect``, then
|
||||
``clip`` is called to clip a line segment. The algorithm handles lines that are fully inside,
|
||||
fully outside, or partially crossing the clipping rectangle. With ``inside`` you can check a single pixel.
|
||||
|
||||
``sub set_cliprect(word x1, word y1, word x2, word y2)``
|
||||
Set the clipping rectangle coordinates, all inclusive. The rectangle must have x1 < x2 and y1 < y2.
|
||||
|
||||
``sub inside(word x, word y) -> bool``
|
||||
Returns true if (x,y) is inside the clipping rectangle set by ``set_cliprect``.
|
||||
|
||||
``sub clip(word x1, word y1, word x2, word y2) -> bool, word, word, word, word``
|
||||
Clip the line segment from (x1,y1) to (x2,y2) against the current clipping rectangle set by ``set_cliprect``.
|
||||
Returns ``visible`` (boolean), plus the clipped coordinates (or (0,0)-(0,0) if not visible).
|
||||
|
||||
|
||||
math
|
||||
^^^^
|
||||
.. index:: pair: Libraries; math
|
||||
@@ -1137,12 +1161,61 @@ See the examples/cx16/bdmusic.p8 program for ideas how to use it.
|
||||
Read the :source:`psg source code <compiler/res/prog8lib/cx16/psg2.p8>` to see what's in there.
|
||||
|
||||
|
||||
sorting (experimental)
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
serial (cx16 only)
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
.. index:: pair: Libraries; serial
|
||||
|
||||
Routines for the serial/wifi card of the Commander X16.
|
||||
Supports up to 2 UART chips. The wifi functionality is handled via the
|
||||
ZiModem (ESP32) command set.
|
||||
|
||||
Generic UART routines:
|
||||
|
||||
``sub detect_uarts() -> uword, uword``
|
||||
Scans the I/O address range for UART chips and returns the addresses
|
||||
of up to two discovered UARTs (or 0 if none found).
|
||||
|
||||
``sub initialize_uart(uword uart_addr)``
|
||||
Initializes the given UART to 921600 baud, 8,N,1 with auto flow control and FIFOs enabled.
|
||||
|
||||
``sub write(uword uart_addr, str data)``
|
||||
Writes the string data to the UART (terminated by 0 byte)
|
||||
|
||||
``sub read_until(uword uart_addr, str match, ^^ubyte buffer, uword max_size) -> uword``
|
||||
Reads bytes from the UART into buffer until the match string is
|
||||
found or max_size bytes have been read. Returns the number of bytes read.
|
||||
|
||||
ZiModem wifi routines:
|
||||
|
||||
``sub zi_initialize(uword uart_addr)``
|
||||
Initializes the ZiModem on the given UART address.
|
||||
|
||||
``sub zi_reset()``
|
||||
Resets the ZiModem connection (``atz``).
|
||||
|
||||
``sub zi_write_cmd(str command)``
|
||||
Writes the given command to the ZiModem followed by CR/LF.
|
||||
|
||||
``sub zi_start_get_file(str filename) -> long``
|
||||
Starts a file download from the given URL and returns the file
|
||||
size. Use ``zi_get_file_chunk()`` repeatedly to download the data.
|
||||
|
||||
``sub zi_get_file_chunk(^^ubyte buffer, uword buffer_size, long remaining_file_size) -> uword``
|
||||
Reads the next chunk of data (up to buffer_size bytes) from an
|
||||
active file download started with ``zi_start_get_file()``.
|
||||
Returns the number of bytes read, or 0 when done.
|
||||
|
||||
``sub zi_get_ip_address() -> str``
|
||||
Returns the IP address of the ZiModem wifi connection.
|
||||
|
||||
Read the :source:`serial source code <compiler/res/prog8lib/cx16/serial.p8>` to see what's in there.
|
||||
|
||||
|
||||
sorting
|
||||
^^^^^^^
|
||||
.. index:: pair: Libraries; sorting
|
||||
|
||||
Various sorting routines (gnome sort and shell sort variants) for byte, word and string arrays.
|
||||
API is experimental and may change or disappear in a future version.
|
||||
**NOTE:** all word and str arrays have to be @nosplit! Words and pointers need to be consecutive in memory for now.
|
||||
**NOTE:** sorting is done in ascending order.
|
||||
Read the :source:`sorting source code <compiler/res/prog8lib/sorting.p8>`
|
||||
|
||||
@@ -1225,11 +1225,16 @@ external subroutines. These last two are described in detail below.
|
||||
|
||||
.. _private-symbols:
|
||||
|
||||
Private subroutines
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
.. index:: pair: Subroutines; Private
|
||||
Private symbols
|
||||
^^^^^^^^^^^^^^^
|
||||
.. index:: pair: Symbols; Private
|
||||
|
||||
You can use the ``private`` keyword (must come before ``inline`` if used) to make a subroutine invisible from outside its block::
|
||||
You can use the ``private`` keyword to hide a symbol from outside its block.
|
||||
Accessing a private symbol from another block results in a compilation error.
|
||||
|
||||
The ``private`` keyword can be applied to the following declarations:
|
||||
|
||||
- **subroutines** (the keyword must come before ``inline`` if used)::
|
||||
|
||||
private sub helper() {
|
||||
; only callable from within this block
|
||||
@@ -1243,7 +1248,24 @@ You can use the ``private`` keyword (must come before ``inline`` if used) to mak
|
||||
; private asmsub
|
||||
}
|
||||
|
||||
Accessing a private subroutine from another block will result in a compilation error.
|
||||
- **external subroutines** (``extsub``)::
|
||||
|
||||
private extsub $FFD2 = CHROUT(ubyte char @A)
|
||||
|
||||
- **structs**::
|
||||
|
||||
private struct Node {
|
||||
byte x
|
||||
byte y
|
||||
}
|
||||
|
||||
- **enums**::
|
||||
|
||||
private enum Color { red, green, blue }
|
||||
|
||||
- **aliases**::
|
||||
|
||||
private alias MyReg = cx16.r0
|
||||
|
||||
|
||||
.. _reusevirtualregs_params:
|
||||
|
||||
+27
-21
@@ -1,10 +1,7 @@
|
||||
TODO
|
||||
====
|
||||
|
||||
excamples/cx16/landscape.p8 is now quite a few bytes BIGGER than before the const optimization of the 'terrain' pointer. Is there a way to prevent this?
|
||||
|
||||
|
||||
Dead Code Elimination BUG in 64tass with nested subroutines
|
||||
Dead Code Elimination bug in 64tass, for nested subroutines
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- When a subroutine contains a nested ``asmsub`` (or possibly a nested ``sub()``), 64tass cannot properly eliminate
|
||||
the outer subroutine if ANY symbol from within it is referenced elsewhere (even if the outer subroutine itself is never called).
|
||||
@@ -15,14 +12,14 @@ Dead Code Elimination BUG in 64tass with nested subroutines
|
||||
|
||||
Weird Heisenbug
|
||||
^^^^^^^^^^^^^^^
|
||||
- BUG: examples/cube3d-float crashes with div by zero error on C64 (works on cx16. ALready broken in v11, v10 still worked)
|
||||
- BUG: examples/cube3d-float crashes with div by zero error on C64 (works on cx16. Already broken in v11, v10 still worked)
|
||||
caused by the RTS after JMP removal in optimizeJsrRtsAndOtherCombinations (replacing it with a NOP makes the problem disappear !??!?).
|
||||
Also observed in the boingball example for the C64 when some code was removed from the start and end.
|
||||
|
||||
|
||||
Future Things and Ideas
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- symboldump: some sort of javadocs generated from the p8 source files (instead of just the function signatures). Use markdown for formatting.
|
||||
- symboldump: some sort of javadocs generated from the p8 source files (instead of just the function signatures). Use markdown for formatting, not html.
|
||||
- why are (interned) strings stored as initialization value in the SymbolTable AND as string nodes in the interned string block? Something seems redundant here?
|
||||
- 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
|
||||
- struct/ptr: implement the remaining TODOs in PointerAssignmentsGen.
|
||||
@@ -30,14 +27,11 @@ Future Things and Ideas
|
||||
- struct/ptr: support for typed function pointers, so that we can call them via functptr^^(args) maybe even without the ^^ operator? (&routine could be typed by default as well then)
|
||||
- struct/ptr: really fixing the pointer dereferencing issues (cursed hybrid beween IdentifierReference, PtrDereferece and PtrIndexedDereference) may require getting rid of scoped identifiers altogether and treat '.' as a "scope or pointer following operator"
|
||||
- struct/ptr: (later, nasty parser problem:) support chaining pointer dereference on function calls that return a pointer. (type checking now fails on stuff like func().field and func().next.field)
|
||||
- allow the value of a memory mapped variable to be address-of another variable, not just a constant number, and maybe even even more complicated constant expressions such as: &uword MyHigh = &mylong+2
|
||||
- Make all constants long by default? or not? (remove type name altogether), reduce to target type implictly if the actual value fits. -> long-consts branch
|
||||
This will break some existing programs that depend on value wrap arounds, but gives more intuitive constant number handling.
|
||||
Can give descriptive error message for old syntax that still includes the type name?
|
||||
- add documentation for more library modules instead of just linking to the source code
|
||||
- sizeof(pointer) is now always 2 (an uword), make this a variable in the ICompilationTarget so that it could be 4 at the time we might ad a 32-bits 68000 target for example. Much code assumes word size addresses though.
|
||||
- romable: should we have a way to explicitly set the memory address for the BSS area (add a -varsaddress and -slabsaddress options?)
|
||||
- romable: fix remaining codegens (some for loops, see ForLoopsAsmGen)
|
||||
- add float support to the configurable compiler targets. Restrictions: just have "cbm-style floats" as an option (to that it can slot into the current float codegen), where all you have to specify is the addresses of AYINT and GIVAYF and FADDT and all their friends.
|
||||
- Change scoping rules for qualified symbols so that they don't always start from the root but behave like other programming languages (look in local scope first), maybe only when qualified symbol starts with '.' such as: .local.value = 33
|
||||
- something to reduce the need to use fully qualified names all the time. 'with' ? Or 'using <prefix>'?
|
||||
@@ -52,39 +46,51 @@ Future Things and Ideas
|
||||
(this is already done hardcoded for several of the builtin functions)
|
||||
- more support for (64tass) SEGMENTS in the prog8 syntax itself? maybe %segment blah in blocks?
|
||||
- ability to use a sub instead of only a var for @bank ? what for though? dynamic bank/overlay loading?
|
||||
- add a way to explicitly set the memory address for the BSS area (``-varsaddress`` and ``-slabsaddress`` options)
|
||||
- BUG: fix the c64 multiplexer example
|
||||
|
||||
|
||||
Romable (%option romable)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
- ForLoopsAsmGen: fix remaining codegens. Three methods use self-modifying code (patching ``cmp #0`` immediates) with no romable-safe alternative:
|
||||
- ``forOverBytesRangeStepGreaterOne`` (byte, abs(step)>=2)
|
||||
- ``forOverWordsRangeStepGreaterOne`` (word, step>=2)
|
||||
- ``forOverWordsRangeStepGreaterOneDescending`` (word, step<=-2)
|
||||
Fix pattern (already used by step-1 methods): add ``if(romable)`` branch that allocates a temp var via ``createTempVarReused``, stores the loop end value into it, and compares against it. Existing self-modifying code stays in ``else`` branch for RAM programs.
|
||||
- BuiltinFunctionsAsmGen: ``callfar`` / ``callfar2`` with non-const bank/addr. Uses self-modifying ``sta +0`` / ``sty +1`` to patch JSRFAR operands. Needs a RAM trampoline approach (copy stub with variable args into RAM, JSR to that).
|
||||
- FunctionCallAsmGen: ``extsub`` with variable bank. Same JSRFAR operand patching issue. Needs RAM trampoline.
|
||||
- Add more test coverage for the romable option.
|
||||
|
||||
|
||||
IR/VM
|
||||
-----
|
||||
^^^^^
|
||||
- getting it in shape for code generation: the IR file should be able to encode every detail about a prog8 program (the VM doesn't have to actually be able to run all of it though!)
|
||||
- maybe change all branch instructions to have 2 exits (label if branch condition ture, and label if false) instead of 1, and get rid of the implicit "next code chunk" link between chunks.
|
||||
- implement more TODOs in AssignmentGen?
|
||||
- add more optimizations in IRPeepholeOptimizer?
|
||||
- **Multi-Level IR Design**: Consider introducing a High-Level IR (HLIR) layer before the current low-level IR to preserve semantics like loop bounds, array indexing, and structure field access.
|
||||
The current IR is effectively "assembly with infinite registers," losing high-level info needed for optimal 6502 instruction selection (e.g., choosing ``LDA addr,X`` vs ``LDA (zp),Y``).
|
||||
Recommendation: Implement a custom HLIR using Kotlin sealed classes (inspired by MLIR dialects but lighter weight).
|
||||
Flow: HLIR (Loops/Arrays) -> Lowering -> Current IR (Ops/Regs) -> Codegen.
|
||||
Don't adopt LLVM (too low-level) or QBE (too simple). Custom HLIR fits Kotlin best and preserves semantic intent for better 6502 codegen.
|
||||
**Split word arrays** are a prime example: currently represented as two separate ``_lsb``/``_msb`` ubyte arrays in the IR, so a single ``words[i] += 50`` expands to 8 byte-level IR instructions (two LOADM, CONCAT, ADD, LSIGB, MSIGB, two STOREM). At the HLIR level this should remain a single word-array augmented assignment; the lowering pass can split it into ``_lsb``/``_msb`` ops (or emit direct word ops for a backend that supports them).
|
||||
The current IR is effectively "assembly with infinite registers."
|
||||
Recommendation when adding non-6502 targets: Implement a custom HLIR using Kotlin sealed classes (inspired by MLIR dialects but lighter weight).
|
||||
Flow: SimpleAst -> HLIR (Loops/Arrays) -> Lowering -> Current IR (Ops/Regs) -> Codegen.
|
||||
Don't adopt LLVM (too low-level) or QBE (too simple). Custom HLIR fits Kotlin best and preserves semantic intent.
|
||||
**Important**: HLIR's value for 6502 is minimal if the backend consumes only the lowered IR. For 6502 to benefit from HLIR, the backend would need to target HLIR directly (bypassing the lowering pass for applicable constructs), adding complexity. HLIR is primarily useful for non-6502 backends (68000) and the VM interpreter.
|
||||
**Split word arrays** are a prime example: currently represented as two separate ``_lsb``/``_msb`` ubyte arrays in the IR, so a single ``words[i] += 50`` expands to 8 byte-level IR instructions (two LOADM, CONCAT, ADD, LSIGB, MSIGB, two STOREM). At the HLIR level this should remain a single word-array augmented assignment; the lowering pass can split it into ``_lsb``/``_msb`` ops (for 6502) or keep it as a word op (for 68000).
|
||||
|
||||
**Missing VM Implementations (VirtualMachine.kt)**
|
||||
- ``IRInlineBinaryChunk`` and ``IRInlineAsmChunk`` - inline chunks cannot be loaded by the VM (VmProgramLoader.kt). Limitation of the current VM design: program is not loaded into memory as data
|
||||
- VM label address loading - ``VmProgramLoader.kt`` throws when it cannot resolve a label address as a value (``"vm cannot yet load a label address as a value"``).
|
||||
|
||||
|
||||
Libraries
|
||||
---------
|
||||
^^^^^^^^^
|
||||
- add a serialio library to interface with the serial/wifi card, see https://github.com/safiire/cx16_wget/blob/master/uart.s
|
||||
- Add split-word array sorting routines to sorting module?
|
||||
- make a list of all floats.* routines that the compiler expects for full float support?
|
||||
- pet32 list_filenames() doesn't check filenames buffer size
|
||||
- make a list of all floats.* routines that the compiler expects for full float support?
|
||||
|
||||
|
||||
Optimizations
|
||||
-------------
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
- Always do const-folding regardless of using -noopt or not. This will break A LOT of unit tests though
|
||||
- inliner: extend multi-value return inlining to support parameterized subroutines. Currently only works for parameterless subroutines. (Void calls with parameters already work if the parameters are unused in the body.)
|
||||
- Port more benchmarks from https://thred.github.io/c-bench-64/ to prog8 and see how it stacks up. (see benchmark-c/ directory)
|
||||
- Compilation speed: try to join multiple modifications in 1 result in the AST processors instead of returning it straight away every time
|
||||
- VariableAllocator: can we think of a smarter strategy for allocating variables into zeropage, rather than first-come-first-served?
|
||||
|
||||
@@ -85,10 +85,11 @@ Tag Effect
|
||||
.. _private-variables:
|
||||
.. index:: pair: Variables; Private
|
||||
|
||||
You can use the ``private`` keyword (must come first, before the datatype and any tags) to make a variable invisible from outside its block::
|
||||
You can use the ``private`` keyword (must come first, before the datatype and any tags) to make a variable or constant invisible from outside its block::
|
||||
|
||||
private ubyte secret = 42
|
||||
private ubyte @shared hiddenvar = 10
|
||||
private const ubyte fixed_value = 99
|
||||
|
||||
This makes the variable only accessible within the block where it's declared. Accessing it from another block will result in a compilation error.
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
%import serial
|
||||
%import textio
|
||||
%zeropage basicsafe
|
||||
%encoding iso
|
||||
|
||||
; use cx16 wifi/serial card to download something from the internet
|
||||
|
||||
main {
|
||||
sub start() {
|
||||
txt.iso()
|
||||
uword uart1, uart2 = serial.detect_uarts()
|
||||
|
||||
if uart1 != 0 {
|
||||
txt.print("uart chips at: ")
|
||||
txt.print_uwhex(uart1, true)
|
||||
txt.spc()
|
||||
txt.print_uwhex(uart2, true)
|
||||
txt.nl()
|
||||
|
||||
serial.zi_initialize(uart1) ; init ZiModem on uart1 (this is how it is configured on the Wifi/serial expansion card)
|
||||
txt.print("\nYour ip address is: ")
|
||||
txt.print(serial.zi_get_ip_address())
|
||||
txt.nl()
|
||||
txt.nl()
|
||||
|
||||
long size = serial.zi_start_get_file("http://mockhttp.org/get?arg=hello-from-prog8-serial")
|
||||
if size>0 {
|
||||
txt.print("downloading file of size: ")
|
||||
txt.print_l(size)
|
||||
txt.nl()
|
||||
|
||||
ubyte[256] buffer
|
||||
while size>0 {
|
||||
uword readsize = serial.zi_get_file_chunk(&buffer, sizeof(buffer), size)
|
||||
size -= readsize
|
||||
for cx16.r0L in 0 to lsb(readsize-1) {
|
||||
txt.chrout(buffer[cx16.r0L])
|
||||
}
|
||||
}
|
||||
|
||||
txt.nl()
|
||||
|
||||
} else {
|
||||
txt.print("file not found\n")
|
||||
}
|
||||
serial.zi_reset()
|
||||
|
||||
} else {
|
||||
txt.print("no uart chips found\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-15
@@ -1,23 +1,44 @@
|
||||
%import textio
|
||||
%zeropage basicsafe
|
||||
%option no_sysinit
|
||||
|
||||
main {
|
||||
sub start() {
|
||||
^^uword ptr1 = 4444 ; should NOT be made const because of data type size >1
|
||||
const ^^uword ptr1b = 4444 ; should give ERROR about not able to make const because of data type size >1
|
||||
^^ubyte ptr2 = 5555 ; should be made CONST because data type sizse == 1
|
||||
ubyte v1, v2
|
||||
|
||||
uword mem1 = memory("mem1", 10, 0) ; should be made CONST because data type sizse == 1
|
||||
^^uword mem1b = memory("mem2", 10, 0) ; should NOT be made const because of data type size >1
|
||||
const ^^uword mem2 = memory("mem2", 10, 0) ; should give ERROR about not able to make const because of data type size >1
|
||||
^^ubyte mem3 = memory("mem3", 10, 0) ; should be made CONST because data type sizse == 1
|
||||
cx16.r0L = 99
|
||||
|
||||
cx16.r0 = ptr1
|
||||
cx16.r1 = ptr1b
|
||||
cx16.r2 = ptr2
|
||||
cx16.r3 = mem1
|
||||
cx16.r4 = mem1b
|
||||
cx16.r5 = mem2
|
||||
cx16.r6 = mem3
|
||||
v1 = inline_paramless_single()
|
||||
txt.print_ub(v1)
|
||||
txt.nl()
|
||||
v1, v2 = inline_paramless_multi()
|
||||
txt.print_ub(v1)
|
||||
txt.spc()
|
||||
txt.print_ub(v2)
|
||||
txt.nl()
|
||||
|
||||
v1 = inline_single( cx16.r0L)
|
||||
txt.print_ub(v1)
|
||||
txt.nl()
|
||||
v1, v2 = inline_multi( cx16.r0L)
|
||||
txt.print_ub(v1)
|
||||
txt.spc()
|
||||
txt.print_ub(v2)
|
||||
txt.nl()
|
||||
}
|
||||
|
||||
inline sub inline_paramless_single() -> ubyte {
|
||||
return cx16.r0L+100
|
||||
}
|
||||
|
||||
inline sub inline_paramless_multi() -> ubyte, ubyte {
|
||||
return cx16.r0L+10, cx16.r0L+20
|
||||
}
|
||||
|
||||
inline sub inline_single(ubyte v) -> ubyte {
|
||||
return v+100
|
||||
}
|
||||
|
||||
inline sub inline_multi(ubyte v) -> ubyte, ubyte {
|
||||
return v+10, v+20
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -4,4 +4,4 @@ org.gradle.parallel=true
|
||||
org.gradle.daemon=true
|
||||
org.gradle.configuration-cache=false
|
||||
kotlin.code.style=official
|
||||
version=12.2-BETA
|
||||
version=12.2-BETA2
|
||||
@@ -194,6 +194,8 @@ class IRFileReader {
|
||||
val name = match.groups["name"]!!.value
|
||||
val zpwish = match.groups["zp"]!!.value
|
||||
val alignment = match.groups["align"]?.value ?: ""
|
||||
val inBssStr = match.groups["inBss"]?.value ?: ""
|
||||
val readonlyStr = match.groups["readonly"]?.value ?: ""
|
||||
|
||||
if('.' !in name)
|
||||
throw IRParseException("unscoped name: $name")
|
||||
@@ -202,7 +204,9 @@ class IRFileReader {
|
||||
val dt = parseDatatype(type, arraysize!=null)
|
||||
val zp = if(zpwish.isBlank()) ZeropageWish.DONTCARE else ZeropageWish.valueOf(zpwish)
|
||||
val align = if(alignment.isBlank()) 0u else alignment.toUInt()
|
||||
return IRStStaticVariable(name, dt, null, arraysize, zp, align, dirty)
|
||||
val inBss = inBssStr == "true"
|
||||
val readonly = readonlyStr == "true"
|
||||
return IRStStaticVariable(name, dt, null, arraysize, zp, align, dirty, inBss, readonly)
|
||||
}
|
||||
|
||||
private fun parseConstants(reader: XMLEventReader): List<IRStConstant> =
|
||||
@@ -240,6 +244,8 @@ class IRFileReader {
|
||||
val value = match.groups["value"]!!.value
|
||||
val zpwish = match.groups["zp"]!!.value
|
||||
val alignment = match.groups["align"]?.value ?: ""
|
||||
val inBssStr = match.groups["inBss"]?.value ?: ""
|
||||
val readonlyStr = match.groups["readonly"]?.value ?: ""
|
||||
|
||||
if('.' !in name)
|
||||
throw IRParseException("unscoped varname: $name")
|
||||
@@ -248,9 +254,11 @@ class IRFileReader {
|
||||
val dt = parseDatatype(type, arraysize!=null)
|
||||
val zp = if(zpwish.isBlank()) ZeropageWish.DONTCARE else ZeropageWish.valueOf(zpwish)
|
||||
val align = if(alignment.isBlank()) 0u else alignment.toUInt()
|
||||
val inBss = inBssStr == "true"
|
||||
val readonly = readonlyStr == "true"
|
||||
val dirty = false
|
||||
val initValue = parseInitValue(dt, value, arraysize)
|
||||
return IRStStaticVariable(name, dt, initValue, arraysize, zp, align, dirty)
|
||||
return IRStStaticVariable(name, dt, initValue, arraysize, zp, align, dirty, inBss, readonly)
|
||||
}
|
||||
|
||||
private fun parseInitValue(dt: DataType, value: String, arraysize: UInt?): IRVariableInitializer? {
|
||||
@@ -327,10 +335,12 @@ class IRFileReader {
|
||||
private fun parseMemoryMapped(line: String): IRStMemVar {
|
||||
val match = IRFormat.MEMORY_MAPPED.matchEntire(line)
|
||||
?: throw IRParseException("invalid MEMORYMAPPED: $line")
|
||||
val (type, arrayspec, name, address) = match.destructured
|
||||
val (type, arrayspec, name, addressAndRest) = match.destructured
|
||||
val readonly = "readonly=true" in addressAndRest
|
||||
val addressStr = addressAndRest.split(" ").first()
|
||||
val arraysize = if(arrayspec.isNotBlank()) arrayspec.substring(1, arrayspec.length-1).toUInt() else null
|
||||
val dt = parseDatatype(type, arraysize!=null)
|
||||
return IRStMemVar(name, dt, parseIRValue(address).toUInt(), arraysize)
|
||||
return IRStMemVar(name, dt, parseIRValue(addressStr).toUInt(), arraysize, readonly)
|
||||
}
|
||||
|
||||
private fun parseSlabs(reader: XMLEventReader): List<IRStMemorySlab> =
|
||||
|
||||
@@ -310,12 +310,16 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
|
||||
emitLine(buildString {
|
||||
append("ubyte[${variable.length}] ${variable.name}_lsb zp=${variable.zpwish} split=true")
|
||||
if(variable.align!=0u) append(" align=${variable.align}")
|
||||
if(variable.inBss) append(" inBss=true")
|
||||
if(variable.readonly) append(" readonly=true")
|
||||
})
|
||||
emitLine("ubyte[${variable.length}] ${variable.name}_msb zp=${variable.zpwish} split=true")
|
||||
} else {
|
||||
emitLine(buildString {
|
||||
append("${variable.typeString} ${variable.name} zp=${variable.zpwish}")
|
||||
if(variable.align!=0u) append(" align=${variable.align}")
|
||||
if(variable.inBss) append(" inBss=true")
|
||||
if(variable.readonly) append(" readonly=true")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -381,6 +385,8 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
|
||||
emitLine(buildString {
|
||||
append("ubyte[${variable.length}] ${variable.name}_lsb=$lsbValue zp=${variable.zpwish} split=true")
|
||||
if(variable.align!=0u) append(" align=${variable.align}")
|
||||
if(variable.inBss) append(" inBss=true")
|
||||
if(variable.readonly) append(" readonly=true")
|
||||
})
|
||||
emitLine("ubyte[${variable.length}] ${variable.name}_msb=$msbValue zp=${variable.zpwish} split=true")
|
||||
}
|
||||
@@ -427,6 +433,8 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
|
||||
emitLine(buildString {
|
||||
append("${variable.typeString} ${variable.name}=$value zp=${variable.zpwish}")
|
||||
if(variable.align!=0u) append(" align=${variable.align}")
|
||||
if(variable.inBss) append(" inBss=true")
|
||||
if(variable.readonly) append(" readonly=true")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -500,7 +508,11 @@ class IRFileWriter(private val irProgram: IRProgram, outfileOverride: Path?) {
|
||||
xml.writeStartElement("MEMORYMAPPED")
|
||||
xml.writeCharacters("\n")
|
||||
for (variable in irProgram.st.allMemMappedVariables()) {
|
||||
emitLine("@${variable.typeString} ${variable.name}=${variable.address.toHex()}")
|
||||
val line = buildString {
|
||||
append("@${variable.typeString} ${variable.name}=${variable.address.toHex()}")
|
||||
if(variable.readonly) append(" readonly=true")
|
||||
}
|
||||
emitLine(line)
|
||||
}
|
||||
xml.writeEndElement()
|
||||
xml.writeCharacters("\n")
|
||||
|
||||
@@ -6,8 +6,8 @@ package prog8.intermediate
|
||||
*/
|
||||
object IRFormat {
|
||||
// Regex patterns for parsing IR text lines
|
||||
val VAR_NO_INIT = Regex("(?<type>.+?)(?<arrayspec>\\[.+?\\])? (?<name>.+) zp=(?<zp>.+?)\\s?(split=(?<split>.+?))?\\s?(align=(?<align>.+?))?")
|
||||
val VAR_INIT = Regex("(?<type>.+?)(?<arrayspec>\\[.+?\\])? (?<name>.+)=(?<value>.*?) zp=(?<zp>.+?)\\s?(split=(?<split>.+?))?\\s?(align=(?<align>.+?))?")
|
||||
val VAR_NO_INIT = Regex("(?<type>.+?)(?<arrayspec>\\[.+?\\])? (?<name>.+) zp=(?<zp>.+?)\\s?(split=(?<split>.+?))?\\s?(align=(?<align>.+?))?\\s?(inBss=(?<inBss>.+?))?\\s?(readonly=(?<readonly>.+?))?")
|
||||
val VAR_INIT = Regex("(?<type>.+?)(?<arrayspec>\\[.+?\\])? (?<name>.+)=(?<value>.*?) zp=(?<zp>.+?)\\s?(split=(?<split>.+?))?\\s?(align=(?<align>.+?))?\\s?(inBss=(?<inBss>.+?))?\\s?(readonly=(?<readonly>.+?))?")
|
||||
val CONSTANT = Regex("(.+?) (.+)=(.*?)")
|
||||
val MEMORY_MAPPED = Regex("@(.+?)(\\[.+?\\])? (.+)=(.+)")
|
||||
val MEMORY_SLAB = Regex("(.+) (.+) (.+)")
|
||||
|
||||
@@ -927,9 +927,12 @@ data class IRInstruction(
|
||||
require(reg2==null || reg2 in 0..99999) {"reg2 out of bounds"}
|
||||
require(reg3==null || reg3 in 0..99999) {"reg3 out of bounds"}
|
||||
// fpReg1/fpReg2 ranges are validated by RegisterNum init block
|
||||
if(reg1!=null && reg2!=null) require(reg1!=reg2 || opcode==Opcode.LOADI) {"reg1 must not be same as reg2"} // note: this is ok for fpRegs as these are always the same type. LOADI is also an exception, hopefully this can work, because it saves a lot of intermediary registers when dereferencing a pointer chain
|
||||
if(reg1!=null && reg3!=null) require(reg1!=reg3) {"reg1 must not be same as reg3"} // note: this is ok for fpRegs as these are always the same type
|
||||
if(reg2!=null && reg3!=null) require(reg2!=reg3) {"reg2 must not be same as reg3"} // note: this is ok for fpRegs as these are always the same type
|
||||
// enforce SSA discipline: output register must differ from input registers
|
||||
// (fpRegs are exempt because float ops always use the same register type and there's no aliasing concern;
|
||||
// LOADI is also exempt because it saves intermediary registers in pointer chain dereferencing)
|
||||
if(reg1!=null && reg2!=null) require(reg1!=reg2 || opcode==Opcode.LOADI) {"reg1 must not be same as reg2"}
|
||||
if(reg1!=null && reg3!=null) require(reg1!=reg3) {"reg1 must not be same as reg3"}
|
||||
if(reg2!=null && reg3!=null) require(reg2!=reg3) {"reg2 must not be same as reg3"}
|
||||
|
||||
val formats = instructionFormats.getValue(opcode)
|
||||
require (type != null || formats.containsKey(null)) { "missing type" }
|
||||
|
||||
@@ -79,7 +79,8 @@ open class IRStNode(val name: String, val type: IRStNodeType)
|
||||
class IRStMemVar(name: String,
|
||||
val dt: DataType,
|
||||
val address: UInt,
|
||||
val length: UInt? // for arrays: the number of elements, for strings: number of characters *including* the terminating 0-byte
|
||||
val length: UInt?, // for arrays: the number of elements, for strings: number of characters *including* the terminating 0-byte
|
||||
val readonly: Boolean = false
|
||||
) : IRStNode(name, IRStNodeType.MEMVAR) {
|
||||
init {
|
||||
require(!dt.isString)
|
||||
@@ -117,7 +118,9 @@ class IRStStaticVariable(name: String,
|
||||
val length: UInt?, // for arrays: the number of elements, for strings: number of characters *including* the terminating 0-byte
|
||||
val zpwish: ZeropageWish, // used in the variable allocator
|
||||
val align: UInt,
|
||||
val dirty: Boolean
|
||||
val dirty: Boolean,
|
||||
val inBss: Boolean = false, // variable should be placed in BSS (RAM), not inline with code
|
||||
val readonly: Boolean = false // variable should be treated as read-only
|
||||
) : IRStNode(name, IRStNodeType.STATICVAR) {
|
||||
init {
|
||||
if(align > 0u) {
|
||||
|
||||
@@ -493,7 +493,7 @@ class Prog8TextDocumentService: TextDocumentService {
|
||||
// The compiler resolves these to target-specific versions automatically
|
||||
val builtinModules = setOf(
|
||||
// Core libraries (available on all targets)
|
||||
"bcd", "buffers", "compression", "conv", "coroutines",
|
||||
"bcd", "buffers", "compression", "conv", "coroutines", "lineclip",
|
||||
"math", "prog8_lib", "prog8_math", "sorting", "strings",
|
||||
"test_stack", "wavfile",
|
||||
|
||||
@@ -503,7 +503,7 @@ class Prog8TextDocumentService: TextDocumentService {
|
||||
|
||||
// CX16 specific modules
|
||||
"adpcm", "bmx", "emudbg", "gfx_hires", "gfx_lores",
|
||||
"monogfx", "palette", "psg", "psg2", "sprites", "verafx",
|
||||
"monogfx", "palette", "psg", "psg2", "serial", "sprites", "verafx",
|
||||
|
||||
// Common aliases
|
||||
"cbm", "memory", "txt"
|
||||
@@ -720,7 +720,7 @@ class Prog8TextDocumentService: TextDocumentService {
|
||||
WorkspaceEdit()
|
||||
}
|
||||
|
||||
private fun getWordAtPosition(document: Prog8Document, position: org.eclipse.lsp4j.Position): String {
|
||||
private fun getWordAtPosition(document: Prog8Document, position: Position): String {
|
||||
// Extract the word at the given position from the document text
|
||||
val lines = document.text.lines()
|
||||
if (position.line < lines.size) {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
# Line clipping library and test programs
|
||||
CC = gcc
|
||||
CFLAGS = -Wall -Wextra -O2
|
||||
LDFLAGS =
|
||||
LIBS =
|
||||
SDL_CFLAGS = $(shell pkg-config --cflags sdl3 2>/dev/null)
|
||||
SDL_LIBS = $(shell pkg-config --libs sdl3 2>/dev/null || echo "-lSDL3")
|
||||
|
||||
# Sources
|
||||
LIB_SRCS = lineclip.c
|
||||
CS_SRCS = test_clip.c
|
||||
VIS_SRCS = clip_visual.c
|
||||
|
||||
# Targets
|
||||
all: test_clip clip_visual
|
||||
|
||||
test_clip: $(CS_SRCS) $(LIB_SRCS)
|
||||
$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $(CS_SRCS) $(LIB_SRCS) $(LIBS)
|
||||
|
||||
clip_visual: $(VIS_SRCS) $(LIB_SRCS)
|
||||
$(CC) $(CFLAGS) $(SDL_CFLAGS) $(LDFLAGS) -o $@ $(VIS_SRCS) $(LIB_SRCS) $(SDL_LIBS)
|
||||
|
||||
clean:
|
||||
rm -f test_clip clip_visual
|
||||
|
||||
.PHONY: all clean
|
||||
@@ -1,264 +0,0 @@
|
||||
/* clip_visual.c - SDL3 visual test for line clipping algorithms
|
||||
*
|
||||
* Draws the clip window and all test cases.
|
||||
* Gray = original line. Green = clipped result (visible). Red = rejected.
|
||||
* Press SPACE to cycle algorithms. Press Q to quit.
|
||||
*
|
||||
* Build: gcc -Wall -Wextra -O2 -o clip_visual clip_visual.c lineclip.c -lSDL3
|
||||
*/
|
||||
#include <SDL3/SDL.h>
|
||||
#include <SDL3/SDL_main.h>
|
||||
#include <stdint.h>
|
||||
#include "lineclip.h"
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
#define WIDTH 1200
|
||||
#define HEIGHT 900
|
||||
|
||||
/* World coordinate range: 0..2000 mapped to screen */
|
||||
#define WORLD_MIN 0
|
||||
#define WORLD_MAX 2000
|
||||
|
||||
/* Clip window: (400,300) to (1600,1400) */
|
||||
#define CLIP_X1 400
|
||||
#define CLIP_Y1 300
|
||||
#define CLIP_X2 1600
|
||||
#define CLIP_Y2 1400
|
||||
|
||||
/* Map world coords to screen coords */
|
||||
static float sx(int16_t x) {
|
||||
return 80.0f + ((float)(x - WORLD_MIN) * (float)(WIDTH - 160) / (float)(WORLD_MAX - WORLD_MIN));
|
||||
}
|
||||
static float sy(int16_t y) {
|
||||
return (float)(HEIGHT - 80) - ((float)(y - WORLD_MIN) * (float)(HEIGHT - 160) / (float)(WORLD_MAX - WORLD_MIN));
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
int16_t x1, y1, x2, y2;
|
||||
const char *name;
|
||||
} TestCase;
|
||||
|
||||
static const TestCase tests[] = {
|
||||
/* Inside the clip window */
|
||||
{ 800, 600, 1200, 1000, "T1 inside" },
|
||||
{ 420, 320, 500, 400, "T2 near TL" },
|
||||
{ 1580, 1380, 1600, 1400, "T3 near BR" },
|
||||
{ 1000, 850, 1000, 850, "T4 point inside" },
|
||||
|
||||
/* Completely outside -- far away */
|
||||
{ -500, -200, -300, 100, "T5 far left" },
|
||||
{ 2200, 1500, 2500, 1800, "T6 far right" },
|
||||
{ 800, -500, 1000, -200, "T7 far above" },
|
||||
{ 1000, 1700, 1200, 2000, "T8 far below" },
|
||||
{ -300, -300, 200, 100, "T9 far TL" },
|
||||
{ 1800, 1600, 2200, 1700, "T10 far BR" },
|
||||
{ -400, 1500, 100, 1800, "T11 far BL" },
|
||||
{ 1800, -300, 2000, 100, "T12 far TR" },
|
||||
|
||||
/* Crossing from outside into window */
|
||||
{ 200, 800, 600, 800, "T13 enter left" },
|
||||
{ 1400, 800, 1800, 800, "T14 enter right" },
|
||||
{ 1000, 100, 1000, 500, "T15 enter top" },
|
||||
{ 1000, 1200, 1000, 1600, "T16 enter bottom" },
|
||||
|
||||
/* Diagonals crossing the window */
|
||||
{ 100, 200, 1800, 1700, "T17 diag TL-BR" },
|
||||
{ 100, 1700, 1800, 200, "T18 diag BL-TR" },
|
||||
{ 1800, 200, 100, 1700, "T19 diag TR-BL" },
|
||||
{ 1800, 1700, 100, 200, "T20 diag BR-TL" },
|
||||
|
||||
/* Very long lines crossing the world */
|
||||
{ 0, 1000, 2000, 1000, "T21 horiz world" },
|
||||
{ 1000, 0, 1000, 2000, "T22 vert world" },
|
||||
{ 0, 0, 2000, 2000, "T23 full world diag" },
|
||||
{ 2000, 0, 0, 2000, "T24 full world diag2" },
|
||||
|
||||
/* Shallow diagonals */
|
||||
{ 200, 800, 1800, 810, "T25 shallow up" },
|
||||
{ 200, 900, 1800, 890, "T26 shallow down" },
|
||||
|
||||
/* Steep diagonals */
|
||||
{ 1000, 100, 1010, 1900, "T27 steep right" },
|
||||
{ 990, 100, 1000, 1900, "T28 steep left" },
|
||||
|
||||
/* Long rejects: parallel but far outside */
|
||||
{ 200, 100, 350, 100, "T29 above parallel" },
|
||||
{ 200, 1550, 350, 1550, "T30 below parallel" },
|
||||
{ 100, 800, 100, 1100, "T31 left parallel" },
|
||||
{ 1800, 800, 1800, 1100, "T32 right parallel" },
|
||||
|
||||
/* Extreme coordinates (within int16 range) */
|
||||
{ 0, 1000, 0, 1001, "T33 x=0 tiny" },
|
||||
{ 2000, 1000, 2000, 1001, "T34 x=2000 tiny" },
|
||||
{ 1000, 0, 1001, 0, "T35 y=0 tiny" },
|
||||
{ 1000, 2000, 1001, 2000, "T36 y=2000 tiny" },
|
||||
|
||||
/* Lines grazing the clip window edges */
|
||||
{ 200, 300, 1800, 300, "T37 graze top edge" },
|
||||
{ 200, 1400, 1800, 1400, "T38 graze bottom" },
|
||||
{ 400, 100, 400, 1600, "T39 graze left edge" },
|
||||
{ 1600, 100, 1600, 1600, "T40 graze right edge" },
|
||||
|
||||
/* Points on clip window corners */
|
||||
{ 400, 300, 400, 300, "T41 pt TL corner" },
|
||||
{ 1600, 300, 1600, 300, "T42 pt TR corner" },
|
||||
{ 400, 1400, 400, 1400, "T43 pt BL corner" },
|
||||
{ 1600, 1400, 1600, 1400, "T44 pt BR corner" },
|
||||
|
||||
/* Lines connecting two distant corners through the window */
|
||||
{ 0, 0, 2000, 2000, "T45 origin to far BR" },
|
||||
{ 2000, 0, 0, 2000, "T46 far TL to far BR" },
|
||||
|
||||
/* Short lines just outside each edge */
|
||||
{ 390, 800, 395, 800, "T47 near left edge" },
|
||||
{ 1605, 800, 1610, 800, "T48 near right edge" },
|
||||
{ 1000, 295, 1005, 295, "T49 near top edge" },
|
||||
{ 1000, 1405, 1005, 1405, "T50 near bottom edge" },
|
||||
};
|
||||
#define NUM_TESTS (sizeof(tests) / sizeof(tests[0]))
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
bool (*clip)(int16_t*, int16_t*, int16_t*, int16_t*,
|
||||
int16_t, int16_t, int16_t, int16_t);
|
||||
} Algo;
|
||||
|
||||
static const Algo algos[] = {
|
||||
{ "CS-float", clip_cohen_sutherland },
|
||||
{ "CS-int16", clip_cohen_sutherland_int16 },
|
||||
{ "Elite-style", clip_elite },
|
||||
};
|
||||
#define NUM_ALGOS (sizeof(algos) / sizeof(algos[0]))
|
||||
|
||||
/* Draw a line */
|
||||
static void draw_line(SDL_Renderer *r, float x1, float y1, float x2, float y2,
|
||||
SDL_FColor c)
|
||||
{
|
||||
SDL_FPoint pts[2] = { { x1, y1 }, { x2, y2 } };
|
||||
SDL_SetRenderDrawColorFloat(r, c.r, c.g, c.b, c.a);
|
||||
SDL_RenderLines(r, pts, 2);
|
||||
}
|
||||
|
||||
/* ----------------------------------------------------------------------- */
|
||||
|
||||
int main(void)
|
||||
{
|
||||
SDL_Window *win;
|
||||
SDL_Renderer *ren;
|
||||
SDL_Event ev;
|
||||
int algo_idx = 0;
|
||||
int quit = 0;
|
||||
|
||||
if (!SDL_Init(SDL_INIT_VIDEO)) {
|
||||
SDL_Log("SDL_Init failed: %s", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
win = SDL_CreateWindow("Line Clipping Visual Test", WIDTH, HEIGHT, 0);
|
||||
ren = SDL_CreateRenderer(win, NULL);
|
||||
if (!win || !ren) {
|
||||
SDL_Log("Window/Renderer failed: %s", SDL_GetError());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/* Clip window rect */
|
||||
SDL_FRect clip_rect = {
|
||||
sx(CLIP_X1), sy(CLIP_Y2),
|
||||
sx(CLIP_X2) - sx(CLIP_X1),
|
||||
sy(CLIP_Y1) - sy(CLIP_Y2)
|
||||
};
|
||||
|
||||
/* Grid lines */
|
||||
SDL_SetRenderDrawColor(ren, 50, 50, 60, 255);
|
||||
for (int i = 0; i <= 2000; i += 200) {
|
||||
SDL_RenderLine(ren, sx(i), sy(WORLD_MIN), sx(i), sy(WORLD_MAX));
|
||||
SDL_RenderLine(ren, sx(WORLD_MIN), sy(i), sx(WORLD_MAX), sy(i));
|
||||
}
|
||||
|
||||
while (!quit) {
|
||||
while (SDL_PollEvent(&ev)) {
|
||||
if (ev.type == SDL_EVENT_QUIT) quit = 1;
|
||||
if (ev.type == SDL_EVENT_KEY_DOWN) {
|
||||
switch (ev.key.key) {
|
||||
case SDLK_ESCAPE: case SDLK_Q: quit = 1; break;
|
||||
case SDLK_SPACE:
|
||||
algo_idx = (algo_idx + 1) % NUM_ALGOS;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Clear */
|
||||
SDL_SetRenderDrawColor(ren, 15, 15, 25, 255);
|
||||
SDL_RenderClear(ren);
|
||||
|
||||
/* Grid */
|
||||
SDL_SetRenderDrawColor(ren, 40, 40, 55, 255);
|
||||
for (int i = 0; i <= WORLD_MAX; i += 200) {
|
||||
SDL_RenderLine(ren, sx(i), sy(WORLD_MIN), sx(i), sy(WORLD_MAX));
|
||||
SDL_RenderLine(ren, sx(WORLD_MIN), sy(i), sx(WORLD_MAX), sy(i));
|
||||
}
|
||||
|
||||
/* Fill clip window */
|
||||
SDL_FColor fill = { 0.1f, 0.12f, 0.18f, 0.7f };
|
||||
SDL_SetRenderDrawColorFloat(ren, fill.r, fill.g, fill.b, fill.a);
|
||||
SDL_RenderFillRect(ren, &clip_rect);
|
||||
|
||||
/* Draw clip window border */
|
||||
SDL_SetRenderDrawColor(ren, 100, 100, 130, 255);
|
||||
SDL_RenderRect(ren, &clip_rect);
|
||||
|
||||
const Algo *algo = &algos[algo_idx];
|
||||
|
||||
/* Draw every test */
|
||||
for (int i = 0; i < (int)NUM_TESTS; i++) {
|
||||
int16_t ox1 = tests[i].x1, oy1 = tests[i].y1;
|
||||
int16_t ox2 = tests[i].x2, oy2 = tests[i].y2;
|
||||
int16_t rx1 = ox1, ry1 = oy1, rx2 = ox2, ry2 = oy2;
|
||||
|
||||
int vis = algo->clip(&rx1, &ry1, &rx2, &ry2,
|
||||
CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2);
|
||||
|
||||
/* Original line in dim gray */
|
||||
SDL_FColor gray = { 0.3f, 0.3f, 0.35f, 0.6f };
|
||||
draw_line(ren, sx(ox1), sy(oy1), sx(ox2), sy(oy2), gray);
|
||||
|
||||
if (vis) {
|
||||
/* Clipped result in green */
|
||||
SDL_FColor green = { 0.15f, 0.9f, 0.3f, 1.0f };
|
||||
draw_line(ren, sx(rx1), sy(ry1), sx(rx2), sy(ry2), green);
|
||||
|
||||
/* Endpoint markers */
|
||||
SDL_FColor marker = { 0.3f, 1.0f, 0.4f, 1.0f };
|
||||
SDL_FRect m = { sx(rx1)-3, sy(ry1)-3, 6, 6 };
|
||||
SDL_SetRenderDrawColorFloat(ren, marker.r, marker.g, marker.b, marker.a);
|
||||
SDL_RenderFillRect(ren, &m);
|
||||
m.x = sx(rx2)-3; m.y = sy(ry2)-3;
|
||||
SDL_RenderFillRect(ren, &m);
|
||||
} else {
|
||||
/* Rejected: red X at midpoint */
|
||||
SDL_FColor red = { 0.8f, 0.15f, 0.15f, 1.0f };
|
||||
float mx = (sx(ox1) + sx(ox2)) / 2;
|
||||
float my = (sy(oy1) + sy(oy2)) / 2;
|
||||
SDL_SetRenderDrawColorFloat(ren, red.r, red.g, red.b, red.a);
|
||||
SDL_FPoint xa[2] = { { mx-5, my-5 }, { mx+5, my+5 } };
|
||||
SDL_FPoint xb[2] = { { mx-5, my+5 }, { mx+5, my-5 } };
|
||||
SDL_RenderLines(ren, xa, 2);
|
||||
SDL_RenderLines(ren, xb, 2);
|
||||
}
|
||||
}
|
||||
|
||||
/* Algorithm name in title bar */
|
||||
char title[128];
|
||||
SDL_snprintf(title, sizeof(title), "%s (%d tests)", algo->name, (int)NUM_TESTS);
|
||||
SDL_SetWindowTitle(win, title);
|
||||
|
||||
SDL_RenderPresent(ren);
|
||||
}
|
||||
|
||||
SDL_DestroyRenderer(ren);
|
||||
SDL_DestroyWindow(win);
|
||||
SDL_Quit();
|
||||
return 0;
|
||||
}
|
||||
@@ -1,266 +0,0 @@
|
||||
/* lineclip.c - Line clipping algorithms
|
||||
*
|
||||
* CURRENT implementation: optimized for SMALLNESS using C's native float.
|
||||
*
|
||||
* FUTURE constraints for a planned integer-only 6502 target implementation:
|
||||
* (These are NOT enforced in the current code!)
|
||||
* -----------------------------------------------------
|
||||
* 1. NO floating point arithmetic.
|
||||
* 2. NO signed modulo operator (%).
|
||||
* 3. Results MUST be strictly inside the clip window.
|
||||
* 4. Return true if visible, false if fully clipped.
|
||||
* 5. Parametric t is scaled by 256.
|
||||
* 6. Keep intermediates in int16_t; use int32_t only when necessary.
|
||||
* Input coords are int16_t (-32767..32767). Clip window is [0,0]-[639,479].
|
||||
* 7. NO recursion — iterative only.
|
||||
* 8. 16-bit signed division IS available. No 32-bit/64-bit division.
|
||||
* Division by 256 is a right shift.
|
||||
*/
|
||||
#include "lineclip.h"
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
|
||||
/* (dist * num) / den — 32-bit intermediate, 16-bit result.
|
||||
* Used by both CS-int16 and Elite-style clipping.
|
||||
*
|
||||
* 6502 Implementation Notes:
|
||||
* ---------------------------
|
||||
* On a plain 6502 this expands to a 16x16→32 multiply followed by a
|
||||
* 32/16→16 division. Neither operation is natively supported, so both
|
||||
* require software routines:
|
||||
*
|
||||
* MUL16x16: shift-add multiply, ~160 cycles
|
||||
* DIV32x16: restoring division, ~200 cycles
|
||||
* TOTAL: ~360 cycles per call
|
||||
*
|
||||
* Optimisation opportunities for 6502:
|
||||
* -------------------------------------
|
||||
* 1. When den is a power of two (e.g. 256), replace the division
|
||||
* with a simple byte swap or right-shift. This saves the entire
|
||||
* DIV32x16 routine (~200 cycles).
|
||||
*
|
||||
* 2. When num is 256 (common in frac_mul(dx, t0, 256)), the 32-bit
|
||||
* multiply reduces to a 16-bit left-shift by 8 (i.e. store dx in
|
||||
* the high word). The result is simply the high byte of the
|
||||
* 32-bit product — no actual multiply loop needed.
|
||||
*
|
||||
* 3. When |dist| < 256 (common for short clip distances), the 16x16
|
||||
* multiply can be reduced to an 8x16 multiply, saving ~40 cycles.
|
||||
*
|
||||
* 4. Sign handling: track the sign separately, do unsigned math on
|
||||
* absolute values, then negate if needed. This avoids signed
|
||||
* multiplication and division entirely, using simpler unsigned
|
||||
* routines.
|
||||
*
|
||||
* 5. Zero-page placement: keep all 6 working variables in zero-page
|
||||
* ($00-$09) to save 2 cycles per access.
|
||||
*
|
||||
* Combined, these optimizations can reduce a single frac_mul call
|
||||
* from ~360 cycles down to ~60-120 cycles depending on the inputs.
|
||||
*/
|
||||
static inline int16_t frac_mul(int16_t dist, int16_t num, int16_t den)
|
||||
{
|
||||
return (int16_t)((int32_t)dist * num / den);
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Cohen-Sutherland: iterative region-code clipping (float) */
|
||||
/* ========================================================================== */
|
||||
|
||||
static inline int _oc(int16_t x, int16_t y, int16_t x1, int16_t y1, int16_t x2, int16_t y2)
|
||||
{
|
||||
int c = 0;
|
||||
if (x < x1) c |= 1;
|
||||
if (x > x2) c |= 2;
|
||||
if (y < y1) c |= 4;
|
||||
if (y > y2) c |= 8;
|
||||
return c;
|
||||
}
|
||||
|
||||
/* After clipping against LEFT/RIGHT edge: x is exactly cx1 or cx2, so
|
||||
* LEFT=RIGHT=0. Only need to test the new y against cy1/cy2. */
|
||||
static inline int _oc_after_x(int16_t y, int16_t cy1, int16_t cy2)
|
||||
{
|
||||
int c = 0;
|
||||
if (y < cy1) c |= 4;
|
||||
if (y > cy2) c |= 8;
|
||||
return c;
|
||||
}
|
||||
|
||||
/* After clipping against TOP/BOTTOM edge: y is exactly cy1 or cy2, so
|
||||
* TOP=BOTTOM=0. Only need to test the new x against cx1/cx2. */
|
||||
static inline int _oc_after_y(int16_t x, int16_t cx1, int16_t cx2)
|
||||
{
|
||||
int c = 0;
|
||||
if (x < cx1) c |= 1;
|
||||
if (x > cx2) c |= 2;
|
||||
return c;
|
||||
}
|
||||
|
||||
bool clip_cohen_sutherland(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2,
|
||||
int16_t cx1, int16_t cy1, int16_t cx2, int16_t cy2)
|
||||
{
|
||||
int16_t ax = *x1, ay = *y1, bx = *x2, by = *y2;
|
||||
int ca = _oc(ax, ay, cx1, cy1, cx2, cy2);
|
||||
int cb = _oc(bx, by, cx1, cy1, cx2, cy2);
|
||||
float dx = bx - ax, dy = by - ay;
|
||||
|
||||
for (;;) {
|
||||
if ((ca | cb) == 0) { *x1 = ax; *y1 = ay; *x2 = bx; *y2 = by; return true; }
|
||||
if ((ca & cb) != 0) return false;
|
||||
|
||||
int co = ca ? ca : cb;
|
||||
int16_t x = 0, y = 0;
|
||||
|
||||
if (co & 8) { x = ax + (int16_t)(dx * (cy2 - ay) / dy); y = cy2; }
|
||||
else if (co & 4) { x = ax + (int16_t)(dx * (cy1 - ay) / dy); y = cy1; }
|
||||
else if (co & 2) { y = ay + (int16_t)(dy * (cx2 - ax) / dx); x = cx2; }
|
||||
else { y = ay + (int16_t)(dy * (cx1 - ax) / dx); x = cx1; }
|
||||
|
||||
if (co == ca) {
|
||||
dx -= x - ax;
|
||||
dy -= y - ay;
|
||||
ax = x; ay = y;
|
||||
ca = (co & 0xC) ? _oc_after_y(ax, cx1, cx2) : _oc_after_x(ay, cy1, cy2);
|
||||
} else {
|
||||
dx -= bx - x;
|
||||
dy -= by - y;
|
||||
bx = x; by = y;
|
||||
cb = (co & 0xC) ? _oc_after_y(bx, cx1, cx2) : _oc_after_x(by, cy1, cy2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Cohen-Sutherland: iterative region-code clipping, pure int16_t */
|
||||
/* Uses frac_mul for the intersection math -- no float operations at all. */
|
||||
/* ========================================================================== */
|
||||
|
||||
bool clip_cohen_sutherland_int16(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2,
|
||||
int16_t cx1, int16_t cy1, int16_t cx2, int16_t cy2)
|
||||
{
|
||||
int16_t ax = *x1, ay = *y1, bx = *x2, by = *y2;
|
||||
int ca = _oc(ax, ay, cx1, cy1, cx2, cy2);
|
||||
int cb = _oc(bx, by, cx1, cy1, cx2, cy2);
|
||||
int16_t dx = bx - ax, dy = by - ay;
|
||||
|
||||
for (;;) {
|
||||
if ((ca | cb) == 0) { *x1 = ax; *y1 = ay; *x2 = bx; *y2 = by; return true; }
|
||||
if ((ca & cb) != 0) return false;
|
||||
|
||||
int co = ca ? ca : cb;
|
||||
int16_t x = 0, y = 0;
|
||||
|
||||
if (co & 8) { x = ax + frac_mul(cy2 - ay, dx, dy); y = cy2; }
|
||||
else if (co & 4) { x = ax + frac_mul(cy1 - ay, dx, dy); y = cy1; }
|
||||
else if (co & 2) { y = ay + frac_mul(cx2 - ax, dy, dx); x = cx2; }
|
||||
else { y = ay + frac_mul(cx1 - ax, dy, dx); x = cx1; }
|
||||
|
||||
if (co == ca) {
|
||||
dx -= x - ax;
|
||||
dy -= y - ay;
|
||||
ax = x; ay = y;
|
||||
ca = (co & 0xC) ? _oc_after_y(ax, cx1, cx2) : _oc_after_x(ay, cy1, cy2);
|
||||
} else {
|
||||
dx -= bx - x;
|
||||
dy -= by - y;
|
||||
bx = x; by = y;
|
||||
cb = (co & 0xC) ? _oc_after_y(bx, cx1, cx2) : _oc_after_x(by, cy1, cy2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* Elite-inspired line clipping (BBC Micro, 6502) */
|
||||
/* Based on the two-stage algorithm from Mark Moxon's Elite deep dive: */
|
||||
/* https://elite.bbcelite.com/deep_dives/line-clipping.html */
|
||||
/* */
|
||||
/* Uses the line equation directly: x = ax + dx*t, y = ay + dy*t. */
|
||||
/* At an X edge: t = (edge - ax) / dx → Δy = dy * t */
|
||||
/* At a Y edge: t = (edge - ay) / dy → Δx = dx * t */
|
||||
/* ========================================================================== */
|
||||
|
||||
/* Test whether a point is inside the clip window */
|
||||
static inline bool elite_inside(int16_t x, int16_t y, int16_t cx1, int16_t cy1,
|
||||
int16_t cx2, int16_t cy2)
|
||||
{
|
||||
return x >= cx1 && x <= cx2 && y >= cy1 && y <= cy2;
|
||||
}
|
||||
|
||||
/* Clip one off-screen endpoint toward the other endpoint.
|
||||
* A moves toward B, B moves toward A (signs handled by caller).
|
||||
* Loops to handle secondary clipping after the first axis is fixed. */
|
||||
static void elite_clip_one(int16_t *ax, int16_t *ay,
|
||||
int16_t cx1, int16_t cy1,
|
||||
int16_t cx2, int16_t cy2,
|
||||
int16_t odx, int16_t ody)
|
||||
{
|
||||
for (int iter = 0; iter < 2; iter++) {
|
||||
int16_t x = *ax, y = *ay;
|
||||
int did_clip = 0;
|
||||
|
||||
if (x < cx1 && odx > 0) {
|
||||
y += frac_mul(cx1 - x, ody, odx);
|
||||
x = cx1;
|
||||
did_clip = 1;
|
||||
} else if (x > cx2 && odx < 0) {
|
||||
y += frac_mul(cx2 - x, ody, odx);
|
||||
x = cx2;
|
||||
did_clip = 1;
|
||||
}
|
||||
|
||||
if (y < cy1 && ody > 0) {
|
||||
x += frac_mul(cy1 - y, odx, ody);
|
||||
y = cy1;
|
||||
did_clip = 1;
|
||||
} else if (y > cy2 && ody < 0) {
|
||||
x += frac_mul(cy2 - y, odx, ody);
|
||||
y = cy2;
|
||||
did_clip = 1;
|
||||
}
|
||||
|
||||
*ax = x;
|
||||
*ay = y;
|
||||
if (!did_clip) break;
|
||||
}
|
||||
}
|
||||
|
||||
/* Elite-style two-stage clipping.
|
||||
* Returns true if any part of the line is visible, false otherwise.
|
||||
* Source: https://elite.bbcelite.com/deep_dives/line-clipping.html */
|
||||
bool clip_elite(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2,
|
||||
int16_t cx1, int16_t cy1, int16_t cx2, int16_t cy2)
|
||||
{
|
||||
int16_t ax = *x1, ay = *y1, bx = *x2, by = *y2;
|
||||
bool a_in = elite_inside(ax, ay, cx1, cy1, cx2, cy2);
|
||||
bool b_in = elite_inside(bx, by, cx1, cy1, cx2, cy2);
|
||||
|
||||
/* Fast accept */
|
||||
if (a_in && b_in) return true;
|
||||
|
||||
/* Quick reject: both off-screen on the same side */
|
||||
int a_code = 0, b_code = 0;
|
||||
if (ax < cx1) a_code |= 1;
|
||||
if (ax > cx2) a_code |= 2;
|
||||
if (ay < cy1) a_code |= 4;
|
||||
if (ay > cy2) a_code |= 8;
|
||||
if (bx < cx1) b_code |= 1;
|
||||
if (bx > cx2) b_code |= 2;
|
||||
if (by < cy1) b_code |= 4;
|
||||
if (by > cy2) b_code |= 8;
|
||||
if ((a_code & b_code) != 0) return false;
|
||||
|
||||
int16_t ddx = bx - ax;
|
||||
int16_t ddy = by - ay;
|
||||
|
||||
/* Clip off-screen endpoints */
|
||||
if (!a_in) elite_clip_one(&ax, &ay, cx1, cy1, cx2, cy2, ddx, ddy);
|
||||
if (!b_in) elite_clip_one(&bx, &by, cx1, cy1, cx2, cy2, -ddx, -ddy);
|
||||
|
||||
/* Final validation */
|
||||
if (!elite_inside(ax, ay, cx1, cy1, cx2, cy2)) return false;
|
||||
if (!elite_inside(bx, by, cx1, cy1, cx2, cy2)) return false;
|
||||
|
||||
*x1 = ax; *y1 = ay; *x2 = bx; *y2 = by;
|
||||
return true;
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
/* lineclip.h - Line clipping algorithms (Cohen-Sutherland, Liang-Barsky, Elite-inspired) */
|
||||
#ifndef LINECLIP_H
|
||||
#define LINECLIP_H
|
||||
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
|
||||
/* All coordinates are signed to support off-screen positions */
|
||||
|
||||
/* Cohen-Sutherland line clipping (float intersection math) */
|
||||
bool clip_cohen_sutherland(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2,
|
||||
int16_t clipx1, int16_t clipy1, int16_t clipx2, int16_t clipy2);
|
||||
|
||||
/* Cohen-Sutherland line clipping (pure int16_t, uses frac_mul internally) */
|
||||
bool clip_cohen_sutherland_int16(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2,
|
||||
int16_t clipx1, int16_t clipy1, int16_t clipx2, int16_t clipy2);
|
||||
|
||||
/* Elite-inspired two-stage clipping (BBC Micro, 6502) */
|
||||
/* Based on: https://elite.bbcelite.com/deep_dives/line-clipping.html */
|
||||
bool clip_elite(int16_t *x1, int16_t *y1, int16_t *x2, int16_t *y2,
|
||||
int16_t clipx1, int16_t clipy1, int16_t clipx2, int16_t clipy2);
|
||||
|
||||
#endif /* LINECLIP_H */
|
||||
@@ -1,176 +0,0 @@
|
||||
/* test_clip.c - Console test program for line clipping algorithms */
|
||||
#include <stdio.h>
|
||||
#include <stdint.h>
|
||||
#include <stdbool.h>
|
||||
#include "lineclip.h"
|
||||
|
||||
/* Clip window: (400,300) to (1600,1400) */
|
||||
#define CLIP_X1 400
|
||||
#define CLIP_Y1 300
|
||||
#define CLIP_X2 1600
|
||||
#define CLIP_Y2 1400
|
||||
|
||||
typedef struct {
|
||||
int16_t x1, y1, x2, y2;
|
||||
int16_t ex1, ey1, ex2, ey2;
|
||||
const char *name;
|
||||
} TestCase;
|
||||
|
||||
static const TestCase tests[] = {
|
||||
/* Inside the clip window */
|
||||
{ 800, 600, 1200, 1000, 800, 600, 1200, 1000, "T1 inside" },
|
||||
{ 420, 320, 500, 400, 420, 320, 500, 400, "T2 near TL" },
|
||||
{ 1580, 1380, 1600, 1400, 1580, 1380, 1600, 1400, "T3 near BR" },
|
||||
{ 1000, 850, 1000, 850, 1000, 850, 1000, 850, "T4 point inside" },
|
||||
|
||||
/* Completely outside -- far away */
|
||||
{ -500, -200, -300, 100, 0, 0, 0, 0, "T5 far left" },
|
||||
{ 2200, 1500, 2500, 1800, 0, 0, 0, 0, "T6 far right" },
|
||||
{ 800, -500, 1000, -200, 0, 0, 0, 0, "T7 far above" },
|
||||
{ 1000, 1700, 1200, 2000, 0, 0, 0, 0, "T8 far below" },
|
||||
{ -300, -300, 200, 100, 0, 0, 0, 0, "T9 far TL" },
|
||||
{ 1800, 1600, 2200, 1700, 0, 0, 0, 0, "T10 far BR" },
|
||||
{ -400, 1500, 100, 1800, 0, 0, 0, 0, "T11 far BL" },
|
||||
{ 1800, -300, 2000, 100, 0, 0, 0, 0, "T12 far TR" },
|
||||
|
||||
/* Crossing from outside into window */
|
||||
{ 200, 800, 600, 800, 400, 800, 600, 800, "T13 enter left" },
|
||||
{ 1400, 800, 1800, 800, 1400, 800, 1600, 800, "T14 enter right" },
|
||||
{ 1000, 100, 1000, 500, 1000, 300, 1000, 500, "T15 enter top" },
|
||||
{ 1000, 1200, 1000, 1600, 1000, 1200, 1000, 1400, "T16 enter bottom" },
|
||||
|
||||
/* Diagonals crossing the window */
|
||||
{ 100, 200, 1800, 1700, 381, 300, 1600, 1425, "T17 diag TL-BR" },
|
||||
{ 100, 1700, 1800, 200, 381, 1400, 1600, 283, "T18 diag BL-TR" },
|
||||
{ 1800, 200, 100, 1700, 1600, 283, 381, 1400, "T19 diag TR-BL" },
|
||||
{ 1800, 1700, 100, 200, 1600, 1425, 381, 300, "T20 diag BR-TL" },
|
||||
|
||||
/* Very long lines crossing the world */
|
||||
{ 0, 1000, 2000, 1000, 400, 1000, 1600, 1000, "T21 horiz world" },
|
||||
{ 1000, 0, 1000, 2000, 1000, 300, 1000, 1400, "T22 vert world" },
|
||||
{ 0, 0, 2000, 2000, 400, 400, 1600, 1600, "T23 full world diag" },
|
||||
{ 2000, 0, 0, 2000, 1600, 400, 400, 1600, "T24 full world diag2" },
|
||||
|
||||
/* Shallow diagonals */
|
||||
{ 200, 800, 1800, 810, 400, 801, 1600, 809, "T25 shallow up" },
|
||||
{ 200, 900, 1800, 890, 400, 899, 1600, 891, "T26 shallow down" },
|
||||
|
||||
/* Steep diagonals */
|
||||
{ 1000, 100, 1010, 1900, 1000, 300, 1001, 1400, "T27 steep right" },
|
||||
{ 990, 100, 1000, 1900, 999, 300, 1000, 1400, "T28 steep left" },
|
||||
|
||||
/* Long rejects: parallel but far outside */
|
||||
{ 200, 100, 350, 100, 0, 0, 0, 0, "T29 above parallel" },
|
||||
{ 200, 1550, 350, 1550, 0, 0, 0, 0, "T30 below parallel" },
|
||||
{ 100, 800, 100, 1100, 0, 0, 0, 0, "T31 left parallel" },
|
||||
{ 1800, 800, 1800, 1100, 0, 0, 0, 0, "T32 right parallel" },
|
||||
|
||||
/* Extreme coordinates (within int16 range) */
|
||||
{ 0, 1000, 0, 1001, 0, 0, 0, 0, "T33 x=0 tiny" },
|
||||
{ 2000, 1000, 2000, 1001, 0, 0, 0, 0, "T34 x=2000 tiny" },
|
||||
{ 1000, 0, 1001, 0, 0, 0, 0, 0, "T35 y=0 tiny" },
|
||||
{ 1000, 2000, 1001, 2000, 0, 0, 0, 0, "T36 y=2000 tiny" },
|
||||
|
||||
/* Lines grazing the clip window edges */
|
||||
{ 200, 300, 1800, 300, 400, 300, 1600, 300, "T37 graze top edge" },
|
||||
{ 200, 1400, 1800, 1400, 400, 1400, 1600, 1400, "T38 graze bottom" },
|
||||
{ 400, 100, 400, 1600, 400, 300, 400, 1400, "T39 graze left edge" },
|
||||
{ 1600, 100, 1600, 1600, 1600, 300, 1600, 1400, "T40 graze right edge" },
|
||||
|
||||
/* Points on clip window corners */
|
||||
{ 400, 300, 400, 300, 400, 300, 400, 300, "T41 pt TL corner" },
|
||||
{ 1600, 300, 1600, 300, 1600, 300, 1600, 300, "T42 pt TR corner" },
|
||||
{ 400, 1400, 400, 1400, 400, 1400, 400, 1400, "T43 pt BL corner" },
|
||||
{ 1600, 1400, 1600, 1400, 1600, 1400, 1600, 1400, "T44 pt BR corner" },
|
||||
|
||||
/* Lines connecting two distant corners through the window */
|
||||
{ 0, 0, 2000, 2000, 400, 400, 1600, 1600, "T45 origin to far BR" },
|
||||
{ 2000, 0, 0, 2000, 1600, 400, 400, 1600, "T46 far TL to far BR" },
|
||||
|
||||
/* Short lines just outside each edge */
|
||||
{ 390, 800, 395, 800, 0, 0, 0, 0, "T47 near left edge" },
|
||||
{ 1605, 800, 1610, 800, 0, 0, 0, 0, "T48 near right edge" },
|
||||
{ 1000, 295, 1005, 295, 0, 0, 0, 0, "T49 near top edge" },
|
||||
{ 1000, 1405, 1005, 1405, 0, 0, 0, 0, "T50 near bottom edge" },
|
||||
};
|
||||
#define NUM_TESTS (sizeof(tests) / sizeof(tests[0]))
|
||||
|
||||
static bool run_test(const TestCase *tc)
|
||||
{
|
||||
int16_t cs_x1 = tc->x1, cs_y1 = tc->y1, cs_x2 = tc->x2, cs_y2 = tc->y2;
|
||||
int16_t ci_x1 = tc->x1, ci_y1 = tc->y1, ci_x2 = tc->x2, ci_y2 = tc->y2;
|
||||
int16_t el_x1 = tc->x1, el_y1 = tc->y1, el_x2 = tc->x2, el_y2 = tc->y2;
|
||||
|
||||
bool cs_vis = clip_cohen_sutherland(&cs_x1, &cs_y1, &cs_x2, &cs_y2,
|
||||
CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2);
|
||||
bool ci_vis = clip_cohen_sutherland_int16(&ci_x1, &ci_y1, &ci_x2, &ci_y2,
|
||||
CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2);
|
||||
bool el_vis = clip_elite(&el_x1, &el_y1, &el_x2, &el_y2,
|
||||
CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2);
|
||||
|
||||
bool pass = true;
|
||||
|
||||
/* CS-float is the reference. Check visibility agrees. */
|
||||
if (ci_vis != cs_vis || el_vis != cs_vis) {
|
||||
printf(" VISIBILITY MISMATCH: CS=%d CSi=%d Elite=%d\n",
|
||||
cs_vis, ci_vis, el_vis);
|
||||
pass = false;
|
||||
}
|
||||
|
||||
/* CS-int16 must match CS-float within ±1 pixel. */
|
||||
if (cs_vis) {
|
||||
if (ci_x1 < cs_x1 - 1 || ci_x1 > cs_x1 + 1 ||
|
||||
ci_y1 < cs_y1 - 1 || ci_y1 > cs_y1 + 1 ||
|
||||
ci_x2 < cs_x2 - 1 || ci_x2 > cs_x2 + 1 ||
|
||||
ci_y2 < cs_y2 - 1 || ci_y2 > cs_y2 + 1) {
|
||||
printf(" CSi!=CS: (%d,%d)-(%d,%d) vs (%d,%d)-(%d,%d)\n",
|
||||
ci_x1, ci_y1, ci_x2, ci_y2, cs_x1, cs_y1, cs_x2, cs_y2);
|
||||
pass = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* Elite must match CS within ±2 pixels (integer truncation vs float rounding). */
|
||||
if (cs_vis) {
|
||||
if (el_x1 < cs_x1 - 2 || el_x1 > cs_x1 + 2 ||
|
||||
el_y1 < cs_y1 - 2 || el_y1 > cs_y1 + 2 ||
|
||||
el_x2 < cs_x2 - 2 || el_x2 > cs_x2 + 2 ||
|
||||
el_y2 < cs_y2 - 2 || el_y2 > cs_y2 + 2) {
|
||||
printf(" Elite!=CS: (%d,%d)-(%d,%d) vs (%d,%d)-(%d,%d)\n",
|
||||
el_x1, el_y1, el_x2, el_y2, cs_x1, cs_y1, cs_x2, cs_y2);
|
||||
pass = false;
|
||||
}
|
||||
/* Elite results MUST be strictly inside the clip window. */
|
||||
if (el_x1 < CLIP_X1 || el_x1 > CLIP_X2 ||
|
||||
el_y1 < CLIP_Y1 || el_y1 > CLIP_Y2 ||
|
||||
el_x2 < CLIP_X1 || el_x2 > CLIP_X2 ||
|
||||
el_y2 < CLIP_Y1 || el_y2 > CLIP_Y2) {
|
||||
printf(" Elite OUTSIDE window: (%d,%d)-(%d,%d) window=(%d,%d)-(%d,%d)\n",
|
||||
el_x1, el_y1, el_x2, el_y2, CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2);
|
||||
pass = false;
|
||||
}
|
||||
}
|
||||
|
||||
printf("%s: %s\n", tc->name, pass ? "PASS" : "FAIL");
|
||||
return pass;
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
int passed = 0, failed = 0;
|
||||
|
||||
printf("Line clipping algorithm tests\n");
|
||||
printf("Clip window: (%d,%d)-(%d,%d)\n\n", CLIP_X1, CLIP_Y1, CLIP_X2, CLIP_Y2);
|
||||
|
||||
for (int i = 0; i < (int)NUM_TESTS; i++) {
|
||||
if (run_test(&tests[i])) {
|
||||
passed++;
|
||||
} else {
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
printf("\n%d passed, %d failed out of %d tests\n",
|
||||
passed, failed, (int)NUM_TESTS);
|
||||
|
||||
return failed > 0 ? 1 : 0;
|
||||
}
|
||||
@@ -152,7 +152,7 @@ statement :
|
||||
;
|
||||
|
||||
|
||||
enum : ENUM identifier '{' EOL? enum_member? (',' EOL? enum_member)* ','? EOL? '}' ; // you can split the values over several lines, trailing comma allowed
|
||||
enum : PRIVATE? ENUM identifier '{' EOL? enum_member? (',' EOL? enum_member)* ','? EOL? '}' ; // you can split the values over several lines, trailing comma allowed
|
||||
|
||||
enum_member : identifier ('=' integerliteral)? ;
|
||||
|
||||
@@ -168,7 +168,7 @@ variabledeclaration :
|
||||
|
||||
|
||||
structdeclaration:
|
||||
STRUCT identifier '{' EOL? (structfielddecl | EOL)+ '}'
|
||||
PRIVATE? STRUCT identifier '{' EOL? (structfielddecl | EOL)+ '}'
|
||||
;
|
||||
|
||||
structfielddecl: datatype identifierlist;
|
||||
@@ -180,7 +180,7 @@ subroutinedeclaration :
|
||||
| extsubroutine
|
||||
;
|
||||
|
||||
alias: 'alias' identifier '=' scoped_identifier ;
|
||||
alias: PRIVATE? 'alias' identifier '=' scoped_identifier ;
|
||||
|
||||
defer: 'defer' (statement | statement_block) ;
|
||||
|
||||
@@ -358,7 +358,7 @@ asmsubroutine :
|
||||
;
|
||||
|
||||
extsubroutine :
|
||||
'extsub' (TAG (constbank=integerliteral | varbank=scoped_identifier))? address=expression '=' asmsub_decl
|
||||
PRIVATE? 'extsub' (TAG (constbank=integerliteral | varbank=scoped_identifier))? address=expression '=' asmsub_decl
|
||||
;
|
||||
|
||||
asmsub_signature : '(' asmsub_params? ')' asmsub_clobbers? asmsub_returns? ;
|
||||
|
||||
@@ -5,7 +5,7 @@ import prog8.code.target.IVirtualMachineRunner
|
||||
import prog8.code.target.VMTarget
|
||||
import prog8.intermediate.*
|
||||
import java.awt.Color
|
||||
import java.awt.Toolkit
|
||||
import java.awt.GraphicsEnvironment
|
||||
import java.io.IOException
|
||||
import kotlin.math.*
|
||||
import kotlin.random.Random
|
||||
@@ -3149,7 +3149,18 @@ class VirtualMachine(irProgram: IRProgram) {
|
||||
}
|
||||
|
||||
fun waitvsync() {
|
||||
Toolkit.getDefaultToolkit().sync() // not really the same as wait on vsync, but there's noting else
|
||||
// note: not a real vsync, but a sleep for approx. the duration of a frame
|
||||
val ms = try {
|
||||
val mode = GraphicsEnvironment.getLocalGraphicsEnvironment().defaultScreenDevice.displayMode
|
||||
if(mode!=null && mode.refreshRate>0) 1000L / mode.refreshRate else 16L
|
||||
} catch(_: Exception) {
|
||||
16L
|
||||
}
|
||||
try {
|
||||
Thread.sleep(ms)
|
||||
} catch(_: InterruptedException) {
|
||||
Thread.currentThread().interrupt()
|
||||
}
|
||||
}
|
||||
|
||||
fun randomSeed(seed1: UShort, seed2: UShort) {
|
||||
|
||||
Reference in new issue
Block a user