mirror of
				https://github.com/irmen/prog8.git
				synced 2025-11-03 19:16:13 +00:00 
			
		
		
		
	unit test for %output library, and docs.
This commit is contained in:
		
							
								
								
									
										106
									
								
								compiler/test/codegeneration/TestLibrary.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								compiler/test/codegeneration/TestLibrary.kt
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
				
			|||||||
 | 
					package prog8tests.compiler.codegeneration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import io.kotest.core.spec.style.FunSpec
 | 
				
			||||||
 | 
					import io.kotest.matchers.shouldBe
 | 
				
			||||||
 | 
					import io.kotest.matchers.shouldNotBe
 | 
				
			||||||
 | 
					import prog8.code.ast.PtBlock
 | 
				
			||||||
 | 
					import prog8.code.ast.PtVariable
 | 
				
			||||||
 | 
					import prog8.code.core.BaseDataType
 | 
				
			||||||
 | 
					import prog8.code.core.DataType
 | 
				
			||||||
 | 
					import prog8.code.target.Cx16Target
 | 
				
			||||||
 | 
					import prog8tests.helpers.compileText
 | 
				
			||||||
 | 
					import kotlin.io.path.readBytes
 | 
				
			||||||
 | 
					import kotlin.io.path.readText
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class TestLibrary: FunSpec({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    test("library compilation") {
 | 
				
			||||||
 | 
					        val src="""
 | 
				
			||||||
 | 
					%address  ${'$'}A050
 | 
				
			||||||
 | 
					%memtop   ${'$'}C000
 | 
				
			||||||
 | 
					%output   library
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					%import textio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main {
 | 
				
			||||||
 | 
					    ; Create a jump table as first thing in the library.
 | 
				
			||||||
 | 
					    uword[] @shared @nosplit jumptable = [
 | 
				
			||||||
 | 
					        ; NOTE: the compiler has inserted a single JMP instruction at the start of the 'main' block, that jumps to the start() routine.
 | 
				
			||||||
 | 
					        ;       This is convenient because the rest of the jump table simply follows it,
 | 
				
			||||||
 | 
					        ;       making the first jump neatly be the required initialization routine for the library (initializing variables and BSS region).
 | 
				
			||||||
 | 
					        ;       btw, ${'$'}4c = opcode for JMP.
 | 
				
			||||||
 | 
					        ${'$'}4c00, &library.func1,
 | 
				
			||||||
 | 
					        ${'$'}4c00, &library.func2,
 | 
				
			||||||
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sub start() {
 | 
				
			||||||
 | 
					        ; has to be here for initialization (BSS, variables init).
 | 
				
			||||||
 | 
					        %asm {{
 | 
				
			||||||
 | 
					            nop
 | 
				
			||||||
 | 
					        }}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					library {
 | 
				
			||||||
 | 
					    sub func1() {
 | 
				
			||||||
 | 
					        cx16.r0L++
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    sub func2() {
 | 
				
			||||||
 | 
					        cx16.r0L--
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}"""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val result = compileText(Cx16Target(), true, src, writeAssembly = true)!!
 | 
				
			||||||
 | 
					        val ep = result.codegenAst!!.entrypoint()
 | 
				
			||||||
 | 
					        val main = ep!!.parent as PtBlock
 | 
				
			||||||
 | 
					        main.name shouldBe "p8b_main"
 | 
				
			||||||
 | 
					        val jumptable = main.children[0] as PtVariable
 | 
				
			||||||
 | 
					        jumptable.name shouldBe "p8v_jumptable"
 | 
				
			||||||
 | 
					        jumptable.type shouldBe DataType.arrayFor(BaseDataType.UWORD, false)
 | 
				
			||||||
 | 
					        jumptable.arraySize shouldBe 4u
 | 
				
			||||||
 | 
					        val asm = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".asm").readText()
 | 
				
			||||||
 | 
					        println(asm)
 | 
				
			||||||
 | 
					        val bin = result.compilationOptions.outputDir.resolve(result.compilerAst.name + ".bin").readBytes().map { it.toUByte() }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        val loadAddr = 0xa050u
 | 
				
			||||||
 | 
					        val startAddr = loadAddr + 0xbu
 | 
				
			||||||
 | 
					        val func1Addr = loadAddr + 0x10u
 | 
				
			||||||
 | 
					        val func2Addr = loadAddr + 0x13u
 | 
				
			||||||
 | 
					        fun msb(addr: UInt) = addr.shr(8).toUByte()
 | 
				
			||||||
 | 
					        fun lsb(addr: UInt) = addr.and(255u).toUByte()
 | 
				
			||||||
 | 
					        fun offset(addr: UInt) = (addr-loadAddr).toInt()+2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // PRG header
 | 
				
			||||||
 | 
					        bin[0] shouldBe lsb(loadAddr)
 | 
				
			||||||
 | 
					        bin[1] shouldBe msb(loadAddr)
 | 
				
			||||||
 | 
					        // first jump table entry; a JMP to start() inserted by prog8 itself
 | 
				
			||||||
 | 
					        bin[2] shouldBe 0x4cu   // JMP
 | 
				
			||||||
 | 
					        bin[3] shouldBe lsb(startAddr)
 | 
				
			||||||
 | 
					        bin[4] shouldBe msb(startAddr)
 | 
				
			||||||
 | 
					        bin[5] shouldBe 0x00u   // 0 padding
 | 
				
			||||||
 | 
					        // second jump table entry (first array element!)
 | 
				
			||||||
 | 
					        bin[6] shouldBe 0x4cu   // JMP
 | 
				
			||||||
 | 
					        bin[7] shouldBe lsb(func1Addr)
 | 
				
			||||||
 | 
					        bin[8] shouldBe msb(func1Addr)
 | 
				
			||||||
 | 
					        bin[9] shouldBe 0x00u   // 0 padding
 | 
				
			||||||
 | 
					        // third jump table entry (second array element!)
 | 
				
			||||||
 | 
					        bin[10] shouldBe 0x4cu  // JMP
 | 
				
			||||||
 | 
					        bin[11] shouldBe lsb(func2Addr)
 | 
				
			||||||
 | 
					        bin[12] shouldBe msb(func2Addr)
 | 
				
			||||||
 | 
					        bin[13] shouldNotBe 0x00u   // no more padding after jump table
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // check start() contents
 | 
				
			||||||
 | 
					        bin[offset(startAddr)] shouldBe 0x20u   // JSR  (to clear_bss)
 | 
				
			||||||
 | 
					        bin[offset(startAddr)+3] shouldBe 0xeau   // NOP
 | 
				
			||||||
 | 
					        bin[offset(startAddr)+4] shouldBe 0x60u   // RTS
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // check func1
 | 
				
			||||||
 | 
					        bin[offset(func1Addr)] shouldBe 0xe6u   // INC
 | 
				
			||||||
 | 
					        // check func2
 | 
				
			||||||
 | 
					        bin[offset(func2Addr)] shouldBe 0xC6u   // DEC
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					})
 | 
				
			||||||
							
								
								
									
										125
									
								
								docs/source/binlibrary.rst
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								docs/source/binlibrary.rst
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,125 @@
 | 
				
			|||||||
 | 
					.. _loadable_library:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					=========================
 | 
				
			||||||
 | 
					Binary Loadable Libraries
 | 
				
			||||||
 | 
					=========================
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					**also called 'Library Blobs'.**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Prog8 allows you to create binary library files that contain routines callable by other programs.
 | 
				
			||||||
 | 
					Those programs can be written in Prog8, BASIC, or something else. They just LOAD the binary library
 | 
				
			||||||
 | 
					file into memory, and call the routines.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					An example of a library file loaded in BASIC on the Commander X16:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.. image:: _static/x16library.png
 | 
				
			||||||
 | 
					    :align: center
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Requirements
 | 
				
			||||||
 | 
					^^^^^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Such a loadable library has to adhere to a few rules:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It can't use zero page variables
 | 
				
			||||||
 | 
					    Otherwise it might overwrite variables being used by the calling program.
 | 
				
			||||||
 | 
					    For systems that have the 16 'virtual registers' cx16.r0-r15 in zero page:
 | 
				
			||||||
 | 
					    these 16 words are free to use. For other systems, only the internal prog8
 | 
				
			||||||
 | 
					    zeropage scratch variables can be used.
 | 
				
			||||||
 | 
					    *note: this may be improved upon in a future version*
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					No system initialization and startup code
 | 
				
			||||||
 | 
					    The library cannot perform any regular "system initialization" that normal
 | 
				
			||||||
 | 
					    Prog8 programs usually perform (such as resetting the IO registers, clearing the screen,
 | 
				
			||||||
 | 
					    changing the colors, and other initialization logic). This would disturb the
 | 
				
			||||||
 | 
					    state of the calling program!  The library can (must) assume that that the calling
 | 
				
			||||||
 | 
					    program has already done all required initialization.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Variable initialization
 | 
				
			||||||
 | 
					    The library still has to initialize any variables it might use and clear
 | 
				
			||||||
 | 
					    uninitialized "BSS" variables! Otherwise the code will not run predictably as prog8 code.
 | 
				
			||||||
 | 
					    So, the library must still have a "start" entrypoint subroutine like any outher prog8 program,
 | 
				
			||||||
 | 
					    that must be called before any other library routine can be called.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Binary output and loaded into a fixed memory address
 | 
				
			||||||
 | 
					    The library must not have a launcher such as a BASIC SYS command, because
 | 
				
			||||||
 | 
					    it is not ran like a normal program.
 | 
				
			||||||
 | 
					    Also, because it is not possible to create position independent code with prog8,
 | 
				
			||||||
 | 
					    a fixed load address has to be decided on and the library must be compiled
 | 
				
			||||||
 | 
					    with that address as the load address. For convenience (and compatibility with older CBM
 | 
				
			||||||
 | 
					    target machines such as the C64 and C128) it's easiest if the resulting library
 | 
				
			||||||
 | 
					    program includes a PRG load header: 2 bytes at the start of the library that contain
 | 
				
			||||||
 | 
					    the load address. This allows BASIC to load the library via a simple ``LOAD "LIB.BIN",8,1`` for example.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					``%output library``
 | 
				
			||||||
 | 
					^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					Most (but not all) of the above requirements can be fulfilled by setting various directives in your
 | 
				
			||||||
 | 
					source code such as %launcher, %zeropage and so on. But there is a single directive that does it correctly for you in one go
 | 
				
			||||||
 | 
					(and makes sure there won't be any initialization code left at all): ``%output library``
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Together with ``%address`` and possibly ``%memtop`` -to tell the compiler what the load address of the library should be-
 | 
				
			||||||
 | 
					it will create a "library.bin" file that fulfills the requirements of a loadable binary library program as listed above.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The entrypoint (= the start subroutine) that must be called to initialize the variables,
 | 
				
			||||||
 | 
					will be the very first thing at the beginning of the library.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Jump table
 | 
				
			||||||
 | 
					^^^^^^^^^^
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For ease of use, libraries should probably have a fixed "jump table" where the offsets of the
 | 
				
			||||||
 | 
					library routines stay the same across different versions of the library. Without needing new syntax,
 | 
				
			||||||
 | 
					there's a trick in Prog8 that you can use to build such a jumptable:
 | 
				
			||||||
 | 
					add a non-splitted word array at the top of the library main block that contains JMP instructions
 | 
				
			||||||
 | 
					and the addresses of the individual library subroutines. Do NOT change the order of the subroutines
 | 
				
			||||||
 | 
					in this table!
 | 
				
			||||||
 | 
					Also note that the Prog8 compiler will insert a single JMP instruction at the very start of the library,
 | 
				
			||||||
 | 
					that jumps to the start subroutine (= the entrypoint of the library program).
 | 
				
			||||||
 | 
					Users of the library need to call this to initialize the variables, so it is a required part of the
 | 
				
			||||||
 | 
					external interface of the library.
 | 
				
			||||||
 | 
					Because the compiler will place the global word array jumptable immediately after this JMP instruction,
 | 
				
			||||||
 | 
					it seems as if the very first entry in the jump table is the jump to the start routine.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Look at the generated assembly code to see exactly what is going on.
 | 
				
			||||||
 | 
					But the users of the library are none the wiser and it just seems as if it is part of the jump table in a natural way :-)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Here is the small example library that was used in the example at the beginning of this chapter::
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    %address  $A000
 | 
				
			||||||
 | 
					    %memtop   $C000
 | 
				
			||||||
 | 
					    %output   library
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    %import textio
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    main {
 | 
				
			||||||
 | 
					        ; Create a jump table as first thing in the library.
 | 
				
			||||||
 | 
					        uword[] @shared @nosplit jumptable = [
 | 
				
			||||||
 | 
					            ; NOTE: the compiler has inserted a single JMP instruction at the start
 | 
				
			||||||
 | 
					            ; of the 'main' block, that jumps to the start() routine.
 | 
				
			||||||
 | 
					            ; This is convenient because the rest of the jump table simply follows it,
 | 
				
			||||||
 | 
					            ; making the first jump neatly be the required initialization routine
 | 
				
			||||||
 | 
					            ; for the library (initializing variables and BSS region).
 | 
				
			||||||
 | 
					            ; Btw, $4c = opcode for JMP.
 | 
				
			||||||
 | 
					            $4c00, &library.func1,
 | 
				
			||||||
 | 
					            $4c00, &library.func2,
 | 
				
			||||||
 | 
					        ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sub start() {
 | 
				
			||||||
 | 
					            ; has to be here for initialization
 | 
				
			||||||
 | 
					            txt.print("lib initialized\n")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    library {
 | 
				
			||||||
 | 
					        sub func1() {
 | 
				
			||||||
 | 
					            txt.print("lib func 1\n")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        sub func2() {
 | 
				
			||||||
 | 
					            txt.print("lib func 2\n")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -220,6 +220,7 @@ Look in the `syntax-files <https://github.com/irmen/prog8/tree/master/syntax-fil
 | 
				
			|||||||
    compiling.rst
 | 
					    compiling.rst
 | 
				
			||||||
    programming.rst
 | 
					    programming.rst
 | 
				
			||||||
    variables.rst
 | 
					    variables.rst
 | 
				
			||||||
 | 
					    binlibrary.rst
 | 
				
			||||||
    libraries.rst
 | 
					    libraries.rst
 | 
				
			||||||
    targetsystem.rst
 | 
					    targetsystem.rst
 | 
				
			||||||
    technical.rst
 | 
					    technical.rst
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -418,6 +418,8 @@ Directives
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
	- type ``raw`` : no header at all, just the raw machine code data
 | 
						- type ``raw`` : no header at all, just the raw machine code data
 | 
				
			||||||
	- type ``prg`` : C64 program (with load address header)
 | 
						- type ``prg`` : C64 program (with load address header)
 | 
				
			||||||
 | 
						- type ``xex`` : Atari xex program
 | 
				
			||||||
 | 
						- type ``library`` : loadable library file (with CBM style load address header) See :ref:`loadable_library`.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.. data:: %zeropage <style>
 | 
					.. data:: %zeropage <style>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,14 +5,8 @@ TODO
 | 
				
			|||||||
    %launcher none
 | 
					    %launcher none
 | 
				
			||||||
    %option no_sysinit
 | 
					    %option no_sysinit
 | 
				
			||||||
    %zeropage dontuse
 | 
					    %zeropage dontuse
 | 
				
			||||||
    You still have to set %address and %memtop yourself to tell prog8 where the library is meant to be placed in memory
 | 
					    You still have to set %address and %memtop yourself to tell prog8 where the library is meant to be placed in memory.
 | 
				
			||||||
    TODO: it's now a raw binary file, should it include the 2-byte PRG header for easy loading from basic as well perhaps?
 | 
					    Result is a .bin file including the 2-byte PRG header that allows you to load it with ,8,1 from basic even on the C64.
 | 
				
			||||||
 | 
					 | 
				
			||||||
- Compiling Libraries: improve ability to create library files in prog8; for instance there's still stuff injected into the start of the start() routine (see translateSubroutine function)
 | 
					 | 
				
			||||||
  AND there is separate setup logic going on before calling it. Make up our mind!
 | 
					 | 
				
			||||||
  Maybe all setup does need to be put into start() ? because the program cannot function correctly when the variables aren't initialized properly bss is not cleared etc. etc.
 | 
					 | 
				
			||||||
  Need to add some way to generate a stable jump table at a given address.
 | 
					 | 
				
			||||||
  Library must not include prog8_program_start stuff either.  Must not require 'start' entrypoint either? Although they need some initialization entry point?
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Make some of the target machine config externally configurable (for 1 new target, the existing ones should stay as they are for the time being)
 | 
					- Make some of the target machine config externally configurable (for 1 new target, the existing ones should stay as they are for the time being)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,14 +14,12 @@ main {
 | 
				
			|||||||
        ;       btw, $4c = opcode for JMP.
 | 
					        ;       btw, $4c = opcode for JMP.
 | 
				
			||||||
        $4c00, &library.func1,
 | 
					        $4c00, &library.func1,
 | 
				
			||||||
        $4c00, &library.func2,
 | 
					        $4c00, &library.func2,
 | 
				
			||||||
        $4c00, &library.func3,
 | 
					 | 
				
			||||||
    ]
 | 
					    ]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    sub start() {
 | 
					    sub start() {
 | 
				
			||||||
        ; has to be here for initialization
 | 
					        ; has to be here for initialization
 | 
				
			||||||
        txt.print("lib initialized\n")
 | 
					        txt.print("lib initialized\n")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -33,8 +31,4 @@ library {
 | 
				
			|||||||
    sub func2() {
 | 
					    sub func2() {
 | 
				
			||||||
        txt.print("lib func 2\n")
 | 
					        txt.print("lib func 2\n")
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    sub func3() {
 | 
					 | 
				
			||||||
        txt.print("lib func 3\n")
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user