Tutorial - Turn a QB64 interpreter into a compiler
#1
Tutorial: How to turn a QB64 interpreter into a compiler.
(WINDOWS ONLY!)

Several of our members have made excellent interpreters in QB64 that run BAS code.  I ported one of mine to QB64, and wanted to take it further and make it an compiler that turn BAS code in standalone EXE's.  Here's a tutorial on how I did it.  With this method you can make your own EXE producing compiler in QB64. 

It's easier to explain the method by just going through the steps of making one, so in this tutorial we will turn a small interpreter into a EXE producing compiler.  Please note - this is not a 'true' compiler, but more like a 'bytecode' one.  The EXE's produced are merely a special interpreter with source coded binded to it - Like RapidQ and other basic compilers out there do.  The EXE's will read itself and run the attached code.  I've attached all the needed source files to this post at the bottom for easier saving.  So...Download all the attached BAS files before we begin.

STEP #1) Compile the MarkExeSize.bas tool to an EXE first.  The interpreter and compiler EXE's we make here will need to be marked by that tool.  You can read what MarkExeSize does in its source code.

(MarkExeBas.bas)
Code: (Select All)
'===============
'MarkExeSize.bas
'===============
'Marks QB64 compiled EXE's with its EXE data size.
'Coded by Dav, JAN/2021

'WINDOWS ONLY!

'This helps facilitate using appended data on the EXE.
'It saves the compiled EXE size to the EXE file, so
'the program can read that info and jump to its data.

'It does this by borrowing some space near the top of
'the EXE file.  It shortens 'This program cannot be run
'in DOS mode.' to 'This program can't run in DOS mode.' and
'uses those 4 gained spaces to save EXE file size instead.

'=======================================================
'Example...after you mark your EXE file, it can do this:
'=======
'OPEN COMMAND$(0) FOR BINARY AS 1  'Open itself up...
'test$ = INPUT$(200, 1) 'grab a little info
'place = INSTR(1, test$, "This program can't") 'look for words
'IF place = 0 THEN PRINT "No data found.": CLOSE: END
'grab exesize info...
'SEEK 1, place + 35: ExeSize& = CVL(INPUT$(4, 1))
'Go there....
'SEEK 1, ExeSize& + 1   'where appended data begins
'=======================================================

'NOTE: Always mark the EXE before appending data to it.
'      If you use EXE compressors, like UPX, mark the EXE
'      AFTER using UPX, not before, otherwise the info won't
'      be read correctly by your program.


SCREEN Pete

PRINT
PRINT "================"
PRINT "MarkExeSize v1.0 - by Dav"
PRINT "================"
PRINT

IF COMMAND$ = "" THEN
    INPUT "EXE to Mark -->", exe$
    PRINT
ELSE
    exe$ = COMMAND$
END IF

IF exe$ = "" THEN END
IF NOT _FILEEXISTS(exe$) THEN
    PRINT "File not found.": END
END IF

OPEN exe$ FOR BINARY AS 1

'find location of place to mark
test$ = INPUT$(200, 1)
place = INSTR(1, test$, "This program can")
IF place = 0 THEN
    PRINT "This file is not markable."
    CLOSE: END
END IF

'jump to location
SEEK 1, place
look$ = INPUT$(19, 1) 'grab a little info

SELECT CASE look$
    CASE IS = "This program cannot"
        'mark/overwrite exe file info file with new info
        PRINT "Marking file "; exe$
        PRINT
        PRINT "EXE files size:"; LOF(1)
        PRINT "Data start loc:"; LOF(1) + 1
        new$ = "This program can't run in DOS mode." + MKL$(LOF(1))
        PUT 1, place, new$
        PRINT: PRINT "Done."
    CASE IS = "This program can't "
        PRINT "EXE already appears to be marked."
        PRINT
        SEEK 1, place + 35: datastart& = CVL(INPUT$(4, 1))
        PRINT "EXE files size:"; LOF(1)
        PRINT "Data start loc:"; datastart& + 1
        PRINT "Size of data  :"; LOF(1) - datastart&
    CASE ELSE
        PRINT "EXE is not markable."
END SELECT

CLOSE


STEP #2)  Compile the sample interpreter.bas to EXE.  This is just an example interpreter.  The main thing is that this interpreter is made to open itself up when run, and load source code attached to itself, instead of loading an external BAS file.  Think of it as the runtime file.  But don't attach any BAS code to it yet, just compile it for now.  (When using your own interpreter you will need to adapt it to load code this way too).


(interpreter.bas)
Code: (Select All)
    'Mini Interpreter runtime.
    'A compiled EXE of this runs BAS code attached to it.
    
    DIM Code$(100) 'space for 100 lines
    
    '==========================================================
    OPEN COMMAND$(0) FOR BINARY AS 1
    place = INSTR(1, INPUT$(200, 1), "This program can't")
    IF place = 0 THEN
        CLOSE: END
    ELSE
        SEEK 1, place + 35: ExeSize& = CVL(INPUT$(4, 1))
    END IF
    '==========================================================
    
    'Make sure something is attached to exe...
    IF ExeSize& + 1 > LOF(1) THEN END
    
    SEEK 1, ExeSize& + 1
    
    Lines = 1
    WHILE NOT EOF(1)
        LINE INPUT #1, c$
        Code$(Lines) = c$
        Lines = Lines + 1
    WEND
    CLOSE 1
    
    
    FOR t = 1 TO Lines
        ExecuteLine Code$(t)
    NEXT
    
    SUB ExecuteLine (cmd$)
        cmd$ = LTRIM$(RTRIM$(cmd$))
        IF LEFT$(cmd$, 1) = "'" THEN EXIT SUB
        IF UCASE$(LEFT$(cmd$, 3)) = "REM" THEN EXIT SUB
        IF UCASE$(LEFT$(cmd$, 5)) = "SLEEP" THEN SLEEP
        IF UCASE$(cmd$) = "BEEP" THEN BEEP
        IF UCASE$(LEFT$(cmd$, 6)) = "COLOR " THEN
            COLOR VAL(RIGHT$(cmd$, LEN(cmd$) - 6))
        END IF
        IF UCASE$(cmd$) = "PRINT" THEN PRINT
        IF UCASE$(LEFT$(cmd$, 7)) = "PRINT " + CHR$(34) THEN
            PRINT MID$(cmd$, 8, LEN(cmd$) - 8)
        END IF
        IF UCASE$(LEFT$(cmd$, 3)) = "CLS" THEN CLS
        IF UCASE$(LEFT$(cmd$, 3)) = "END" THEN END
    END SUB
    

STEP #3) Compile the compiler.bas to EXE.  This little programs whole job is to combine the interpreter+source code together.  But - It will have the interpreter runtime attached to it eventually, like the interpreter has code attached to it.  We will attach that later.  For now just compile it...
(compiler.bas)
Code: (Select All)
    'Mini Compiler example
    
    PRINT
    PRINT "A Mini .BAS Compiler"
    PRINT "Compile .BAS to .EXE"
    PRINT
    INPUT "BAS to open ->", in$: IF in$ = "" THEN END
    INPUT "EXE to make ->", out$: IF out$ = "" THEN END
    
    'First see if this EXE is marked...
    OPEN COMMAND$(0) FOR BINARY AS 1
    place = INSTR(1, INPUT$(200, 1), "This program can't")
    IF place = 0 THEN CLOSE: END
    
    'Grab EXE size info
    SEEK 1, place + 35: ExeSize& = CVL(INPUT$(4, 1))
    'Make sure data attached...
    IF ExeSize& + 1 > LOF(1) THEN END
    
    'Jump to data
    SEEK 1, ExeSize& + 1
    
    'Extract data, make EXE file...
    OPEN out$ FOR OUTPUT AS 2
    outdata$ = INPUT$(LOF(1) - ExeSize&, 1)
    PRINT #2, outdata$;: outdata$ = ""
    
    'Add/attach BAS code to EXE
    OPEN in$ FOR BINARY AS 3
    outdata$ = INPUT$(LOF(3), 3)
    PRINT #2, outdata$;
    
    CLOSE
    
    PRINT "Made "; out$
    
    END
    

OPTIONAL STEP:   At this point you could run UPX on those EXE's to reduce their size down to about 500k.  You will have to download UPX from off the internet.  I use it a lot.  Works well on QB64 generated EXE's.  Make sure if you do this step, that you do it right here - BEFORE using MarkExeSize on them.

STEP #4) Now use the MarkExeSize.exe tool on both the interpreter.exe and compiler.exe programs.  It saves their EXE size in the EXE's.   IMPORTANT: This is a needed step.  Without it, the EXE's won't know how to open a file attached to them.

STEP #5)  Now it's time to make the mini.exe compiler program.   Drop to a command prompt, into the folder where the new EXE's are, and combine both the compiler.exe+interpreter.exe files like this, making a new file called mini.exe:

copy /b compiler.exe+interpreter.exe mini.exe

If all went well, You just made a new EXE file called mini.exe. It's the whole compiler that contains the interpreter runtime too.  Run mini.exe, and you can now compile the demo.bas below.  It will generate a demo.exe out of it.   The interpreter.exe & compiler.exe are no longer needed - mini.exe is the only thing needed to make the EXE files from BAS code.

(demo.bas)
Code: (Select All)
    REM Sample program
    COLOR 3
    PRINT "Hit any key to clear..."
    SLEEP
    BEEP
    CLS
    COLOR 15
    PRINT "Cleared!"
    END

Final comments:  The example here is just a simple interpreter, just to show you how to do yours.  Be aware that unless you encode/decode your source code on the interpreter, people will be able to open up your EXE and see the source code, so I would put in an encoding/decoding method in your interpreter.

Try building this sample first, and you will see how easy it is to turn your interpreter into a byte-code compiler using QB64.  Start your own programming language!

Have fun!

 - Dav


.bas   markexesize.bas (Size: 2.64 KB / Downloads: 59)

.bas   interpreter.bas (Size: 1.3 KB / Downloads: 58)

.bas   compiler.bas (Size: 829 bytes / Downloads: 58)

.bas   demo.bas (Size: 113 bytes / Downloads: 55)

Find my programs here in Dav's QB64 Corner
Reply
#2
Wow this will come in handy when I get my interpreter updated. Oh the projects on my ToDo List are many!

Thanks Dav, love those games of yours as well!
Reply
#3
I am testing the basic interpreter yabasic. it allows like the excellent old RapidQ to combine the interpreter with the source code program converted into pseudocode to generate a standalone executable. the version of yabasic that I have compiled has a size of about 302 kb. which allows to generate light executables easily distributable...
Reply
#4
It's been a long while since I've checked out yabasic.  Looks like it has grown well.  Gonna play with it some over the weekend.  Thanks...

- Dav

Find my programs here in Dav's QB64 Corner
Reply




Users browsing this thread: 3 Guest(s)