The latest revision of M2000 make the interpreter faster, of about 17% (less time to compute), so we can say we have a speedup of x1.2 times,(1/0.83=1.204) so we can say the new revision make the Interpreter 20% faster.
How this done?
M2000 Interpeter always execute the source as is. Which means there is no conversion to a tokenized form. Also there is no AST construct. That is no very bad, for a language which are made for expansion as we go, for the last 26 years). The purpose for using the M2000 Interpreter surely is not for \time critical programs. Much of the time consuming work done from objects and internal code which can do things like rotation of bitmaps. So all the Interpreter is a function with a series of helping functions. The code run consuming the string which hold it. Some time maybe I change this, but for now when we do this Print 12+10 first the Print statement removed from string, then the Expression evaluation stage get the first number 12 then get the plus sign and looking for another number or another sign before the number (you can do this 12+-+-12 and is ok)) or an open parenthesis and so on.
A numeric expression may have strings on it: "aaa" is a string expression, but "aaa">"bbb" is a Boolean one (a numeric for M2000). So before this revision and before the 11th version, there was strictly two kind of expressions, the string and the numeric. When we start the numeric evaluator, there was a looking ahead mechanism to find if there is a logic operator, when strings find in front at the expression, so at this point conclude that in front exist a numeric expression or a string one. So if it is a string return without exclude any value (literal) or name (variable or constant). For versions lower than 11, the string variables and functions was all with a $ as suffix, so alfa$ was treated as string, and this was the same with a alfa$(), where the "things" inside parenthesis skipped from the look ahead system as irrelevant for determinate the type of expression. So from version 11 we may have an alfa variable as a string one. That was a difficult situation for holding the IsExp() function for numeric (and now string results) and the string only IsStrExp() function. Each of these functions knows all the internal functions, the numeric for IsExp() and string fot IsStrExp(). So there was a logical() function which find looking ahead if there is a string expression and and a logic operator. If not return false without removing anything. Now for the Revision 20 of version 14, this function drop the look ahead part and also the internal string comparison part. The logical() function called from the IsExpA() function inside IsExp() function, for each value which we want to process. So now we don't have the look ahead part because now the process done for strings too inside IsExpA(). The IsStrExp() works because some time we want to get a parameter only as string. This IsStrExp() call the IsString() and get the value and then see if there is the plus operator to get another string, to concatenate them, and repeat this until no other operation exist. IsExpA() which work for a number of different types, work nice for strings too. If we place a value an operator and a value and then an operator plus and a string then we get error. I do that because this can be a mistake, So 10+10+"ok" give an error, but 10+"ok"+10 give a string 10ok10, and also "ok"+10+10 give a string ok1010. IsExp() the shell of the numeric/object expressions, works for all numeric types plus the complex numbers (1,-2i) or (a,b i) are complex notation for making complex numbers, also works for dates, bytes, long long and biginteger (which are objects), plus with Group type of objects which have operators as functions so a A+A can be a sum of two numeric types including complex and biginteger, a concatenation of two string values or a produce of an object from object A which knows the operator "+" as a function (which call the evaluator) and pass the value of expression at part after the "plus" operator.
To do this functionality I have to make some changes in the code, to work as expected. Maybe I am not 100% for all situation, but at the long run I will find any mistake or not prepared code to this level of functionality.
Another two new things about this revision:
- We can use Ctrl / (also Ctrl 4 works the same) to apply comment or uncomment lines of code (one or more). Because / may change to other keys (other virtual keys), I improve the code to find this for the specific keyboard language in use. Also Keypress() function now have a ! switch to address the same situation we want a character code to be pressed but we didn't know the scan code, but the system know it and we just supply the parameter as character Unicode code.
Also we can use Ctrl 2 for lower case (no selected text, or convert also for selected text), and Ctrl 1 for upper case (no selected text, or convert also for selected text). So if you want fast capital key you press Ctrl 1 and we get the caps lock to ON for sure without looking the keyboard.
- The last thing do magic or something like this. We want to open a second m2000 program in front of our program, through our program. Before this magic, the new program open, but if our own program got the focus the other program go to background, and our program hide it. That is not bad, because all programs do that. But what if we want the new program which we start from our program want to stay in front of our program? To do that we pass the hWnd (the window handler) of the BACK (the real form of M2000 console) to the new program and that program has to run the Show statement using this number as a parameter (this is new, the Show get optional a parameter). You can see how this work with the Clock module (which run the Clock1, in another M2000.exe). So when we open the clock the form of the clock get a parent form, our console form, so Windows manager always show the clock in front of the parent (our console form). It is the same as the Help form, or the Control form, or the user forms which we make as children of the console form. If we start the Form44 module which make three windows we can put the Clock (which run on own M2000.exe) between the user forms because these have the same parent, the console form. If out form terminate, the other program just do nothing about it, like there is no parent for it; We don't have family problems here;
Another example is the Compiler. When we compile one of the examples we ask to use M2000 Transpiler (compile to M2000 starements) or Virtual Machine (byte code for a virtual machine - now running as a M2000 program also). If we choose Transpiler we Asked if we want the execution done in the console or not (by default is not) so if we press enter a new M2000.exe start and run as child window in front of our console (with transparency also). I am thinking to make a way to make forks and these can be used to form a parent child connection. I am thinking about the way the forked code to access global variables of the parent code.
I work with ChatGpt to understand the music terms and how they work, and to test my interpreter. This is music generated from M2000 and composer was ChatGpt (small version of the known Ode to Joy)
CONST Acoustic_Bass = 33 TEMPO=960*1.2 VOLUME 80 PLAY 1, 0
' Notes: 12, 7 physical and 5 semitones ' You can use C# or D♭ (is the same). The ♭ character from CTRL + 3
' Octave 4 (5th) ' we can change octave once for next notes too, until a new change happen. 'Octaves 0 to 9
' VALUE PART ' @1 TEMPO - IF YOU OMIT THEN IT IS @1 ' @2 TEMPO/2 ' @3 TEMPO/4 ' @4 TEMPO/8 ' @5 TEMPO/16 ' @6 TEMPO/32 ' dotted note @2+ (@2 plus @3) ' double dotted @2++ (@2 plus @3 plus @4) ' we can add Velocity (Volume), V0 to V127, but this change until next V excuted
' You can make TIES adding values (max 20), so @4@2 is a TEMPO/8+TEMPO/2 duration for one note. ' A ! means legato, but a !95 is a staccato 95% - You can assign it per note if you like (or at SCORE, 4th parameter for global in score)
' You can make Tuplets: [CD]3@3 two notes played for 3 x quarters. We compute scale=(3/4)/(2)=3/8, so C played for 1x3/8 and D played for 1x3/8 ' Tuplets can't have tuplets inside (for this version). Tuplets may have staccato/velocity for all individul notes ' Value for tuplet may have Multiplier before or not. A tuplet without defined value and or multiplier has value 1 Tempo by default. ' At the end of tuplet Staccato, Volume, Octave restored.
' USE SPACE FOR SILENCE - ALSO SILENCE HAVE VALUES ' SO A SPACE IS A SILENCE FOR A TEMPO TIME. ' A SPACE@4 IS A SILENCE FOR TEMPO/8
' Volume/Velocity V0 TO V127 - you can place it before note or after value (value may missing, is 1/1 by default) for the note to take account the change. ' Inside Tuplets not use this at start - only after value (value may missing, is 1/1 by default) ' For defining Volume for Tuplets you can do that aftet the value of Tuplet and before Staccato. so [C@3 @3E@3]2V60!75 place V60 for Tuplet and staccato 75%. See there is only multiplier, so this Tuplet has duration 2xTEMPO. Space inside is the silence of a value 1/4*scale. Scale = 2/(3/4)=8/3, so the 1/4*8/3=8/12=2/3. C@3 has finally Tempo*2/3*0.75 duration time partitura="V80" partitura+="E4@3E@3F@3G@3" partitura+="G@3F@3E@3D@3" partitura+="C@3C@3D@3E@3" partitura+="E@4D@4D@2"
LOCALE 1033 OPEN "DURATION_TEST.TXT" FOR OUTPUT AS #F PROFILER PLAY 1,1,2,1,3,1,4,1,5,1,6,1,7,1,8,1 EVERY TEMPO/128 { WRITE #F, PLAYNOTE(1),PLAYNOTE(2),PLAYNOTE(3),PLAYNOTE(4), WRITE #F,PLAYNOTE(5),PLAYGATE(5) ,PLAYNOTE(6),PLAYNOTE(7),PLAYNOTE(8), PLAYTUPLET(8),PLAYGATE(8), WRITE #F,TIMECOUNT IF NOT PLAYSCORE THEN EXIT } CLOSE #F WIN "NOTEPAD", DIR$+"DURATION_TEST.TXT"
When we do some difficult things we end with a hang process.
This is the right program which is not hang the M2000 Envirnoment:
Declare global GetMem2 lib "msvbvm60.GetMem2" {Long addr, Long retValue} Variant A=1000& Integer B=0 Call Void GetMem2(varptr(A), varptr(B)) Print B
So now we think that maybe can work using different signature:
// this is the wrong function/signature declare global GetMem2_Bad lib "msvbvm60.GetMem2" {Long addr, &retValue} Variant A=1000& Integer B=0 Call void GetMem2_Bad(varptr(A), &B) ? B
This hang the Interpreter/Environment
So we run another M2000.exe and put the following code on a module, say A (Edit A then Copy this, press Esc and write A and press enter)
This program finds files with M2000 extension in TEMP folder. Every time we start M2000.exe a new PID (process ID) assigned from operating system and M2000 write an empty file with the name then number of PID and M2000 as extension. So We get the current PID using the GetCurrentProcessID of Kernel32.dll and then we get all *,M2000 files, enumerate them, excluding our PID and sending Taskkill by PID. Also we delete the associate M2000 file. Last we have to delete the TMP type file which VB6 (not the M2000.dll) make it. We can't delete these TMP file if they are locked. TMP files are not of the same type for every program. Operating system produce the names which guarantee that are not used. So we don't now from the M2000.dll which name the tmp file procuced. For this reason we delete all not locked TMP files. Ypu can ask googles AI "can we delete TMP files in TEMP directory if they ar not locked?" and you get YES, plus the story about that.
The simulated hang program before can closed from the taskbar, which we terminate the M2000.exe which use the M2000.dll as a com object. The TMP file also erased but the M2000 not erased. So the program here will delete only the file (the program are not exist as a process at this point)
The DOS statement using a semi colon at the end executed in a hidden window.
Use Win m2000 to start another M2000.exe (M2000 Interpreter)
or use
USE alfa, "paremeter", 2000
to open another M2000.exe, run alfa.gsb and put these two parameters in the stack of the that interpreter. We can send only strings and numbers from that call. We can use named PIPES, so we can send a pipe name, and we can make alfa the client, and the program which "use" it as server. Also the pipename can be as something specific and all clients known the name from the start (hardcoded).
Named pipes can be used to provide communication between processes on the same computer or between processes on different computers across a network. If the server service is running, all named pipes are accessible remotely
Write HELP USE in command line in M2000 Environment to see about the use of pipes.
This program can't kill process from another computer from the LAN.
ourPID now is string. The new upgrade of M2000 not convert a numeric variable to string unless is a + sign (so maybe it is part of a string expression)
Declare GetCurrentProcessId Lib "kernel32.GetCurrentProcessId" As Long // this is our ProcessId string ourPID=GetCurrentProcessId()// convert to string` // get current directrory m$=dir$ // set TEMP folder as current folder dir temporary$ // empty menu array menu // using + we send filenames to menu$() array files + "m2000" if menuitems>0 else menu: exit // We get a copy (menu$() is not an array is a function) // when we use it without parameter return a an array a=menu$() menu k=each(a) while k if array(k)<>ourPID then Print "Erase "+array$(k) // now we kill the PID and erase the (pid).M2000 dos "taskkill /F /PID "+array$(k); wait 100 dos "del "+quote$(dir$+array$(k)+".m2000"); end if end while // delete all tmp files which are not locked // because one tmp file produced by the VB6 dll, and we didn't now which are dos "del /A:-R "+quote$(temporary$+"*.tmp"); dir m$
We make a form, with a context menu (see the three parallel lines at the right up corner. We place in form an Image control for using by OpenGl, three Textbox as Spinner Controls (alter values using up/down arrow or Shift up/down for fine tune)m and one "M2000.ctxNineButton" which is a ctxNineButton.ctl (included in M2000.dll) from Vladimir Vissoultchev (nickname: wqweto, https://github.com/wqweto/NinePatch). In M2000 we can use external controls, using the Delcare VarName Type "name as register" FORM name_of_form or image (image control can hold controls too).
Especially for ctxNineButton the GUIM2000 form (the basic form for user UI in M2000), hold the functionality to perform the animations (fade in out for caption change, transparent and light transitions) which you see in the video. Also I use the TabStop to include it in the list of "tab" used controls: There are 5 in total, one is the form (with down arrow we open the context menu), three more the textboxes, and one the blue button of type NineButton.
We make the controls for X,Y,Z with consolas font, and large enough to see them. Also we can change the type of decimal point (changing UTC to GTB time (for Greece), also I change the locale type). The XYZ not only used to pass values but for showing values too.
The OpenGl scene run on a Thread, not a really thread but an M2000 type of thread (based on time intrevals). Threading system use either a Concurrent or a Sequential mode, here the default Sequential mode run. So when the form is up, only threads and events run. An event has always priority over thread. But all events are run in short time. The same for controls. When we write in a control the event of a click is very short, so the control not use the cpu time, so our threads run smoothly.
The task for OpenGl is to show not only the Scene, as the 3D objects plus the HUD type text (see first line) and the 3D text (two lines - one traveling both horizontal directions in turn), but the background which we take a snapshot every time the scene constructed. So we draw lines on Form, and take the part of the form behind Image1 and place it in an OpenGl texure (the Background). Also we place a second Texture which can move and rotate everywhere (the background is always at the same viewport like the HUD text). The HUD Text has 20 lines always, and the class has ready support for latin and greek letters. The "library" (or class if you prefer) written with the help of ChatGPT although the DIBsection used for loading fast the texture are mine (I have to say I didn't ask for that, so ChatGPT maybe can give something better). The crucial part was the understanding of OpenGL which was zero for me, so for years I didn't included on M2000 code. Now there is something basic.
This is the code of cOpenGl.cls. You can see the Public interface (just search Public to see the methods (functions or subs) which tagged Public. Those methods we can call from M2000. There are other objects like Sphere Torus Pyramid Cone Cylinder.
locale 1033 cls,0 escape off back{ cls 0 } declare form1 form method form1, "move", 0,0, 12000, 6600 gColor=color(256*0.2, 256*0.6, 256*0.9) Layer form1 { cls 15 font "consolas" window mode, scale.x, scale.y; gradient gColor hold ' push the background to store (we use restore to bring back the background) form1.scaleX=scale.x form2.scaleY=scale.y }
With form1, "titleheight" as titleheight, "id" as Id(), "MenuEnabled" as MenuEnabled(), "ListSelected" as Checked() long ret module EnableRotation { MenuEnabled(tag1)=false MenuEnabled(tag1+1)=true } module DisableRotation { MenuEnabled(tag1+1)=false MenuEnabled(tag1)=true } boolean clearplease Function Form1.infoclick(new it) { select case val(id(it)) case 500 if pushbutton then call local scenedrive case 600 if pushbutton else call local scenedrive case 800 call local form1.click() case 850 Checked(tag2+1)=not Checked(tag2+1) call local opacity case 900 clearplease=true case 1000 {after 100 {ret=ask("Hello", "Help", "Ok", "")} } case 2000 method form1,"CloseNow" end select } Function Form1.InfoChecked(new it) { if val(id(it)) =800 then if checked(it)and locale<>1033 then call local form1.click() else.if not checked(it)and locale=1033 then call local form1.click() end if else.if val(id(it)) =850 then call local opacity end if } module opacity { if Checked(tag2+1)then method form1, "Opacity", 180 else method form1, "Opacity", 255
With image1, "TabStop", false, "Default", true With form1, "visible" as visible, "blink", 300, "blinktimes", 12, "NoFocus" as ReadNoFocus Boolean Free
Layer Image1 { cls color(256*0.2, 256*0.6, 256*0.9) declare og opengl With og, "Ready" as Ready } method og, "ReStartGL", Textures2d:=2 'method og, "SetLoadPicture", dir$+"myjpg.jpg", number:=1 method og, "SetLoadPicture", img$, number:=1 img$="" 'get memory back ? type$(og) angleX=13.4~ angleY=21.4~ angleZ=0~ Single PosGreek=2.6 d=-1~ Print "OpenGl ready", ready if not ready then exit Layer form1 { refresh 25 release } function button1.click { free~ if free then bCaption$="status: free rotation" else bCaption$="status: no free rotation" } function form1.unload { threads erase } function form1.click { locale 2065-locale spX$=dec1$(angleX) spY$=dec1$(angleY) spZ$=dec1$(angleZ) Checked(tag2)=locale=1033 } function form1.blink { flush if scene_run>0 then thread scene_run interval 30 } scene_run=0 pushbutton=true def dec1$()=str$((number mod 3600)/10,"#0.0") ' this is for ValidString module clearInput { local o$ o$=filter$(v$, "-1234567890"+locale$(14)) if o$<>"" then v$=filter$(v$, o$) } module commonKeyDown { if pushbutton then call local scenedrive } module commonValid (new &AngleN) { try { local single alfa alfa=val(v$, locale$(14))*10:angleN=alfa mod 3600~:alfa=angleN } if valid(alfa)then v$=dec1$(alfa)else where=1000 } ' XXXXXXXXXXXXXXXX function InpAngleX.keydown {call local commonKeydown} function InpAngleX.click { if pushbutton then call local scenedrive } function InpAngleX.SpinnerValue(new v) {spX$=dec1$(v)} function InpAngleX.ValidString(new &v$, &where) { call local clearInput call local commonValid, &angleX } 'YYYYYYYYYYYYYYYYYYYY function InpAngleY.keydown {call local commonKeydown} function InpAngleY.click { if pushbutton then call local scenedrive } function InpAngleY.SpinnerValue(new v) {spY$=dec1$(v)} function InpAngleY.ValidString(new &v$, &where) { call local clearInput call local commonValid, &angleY } 'zzzzzzzzzzzzzzzzzzz function InpAngleZ.keydown {call local commonKeydown} function InpAngleZ.click { if pushbutton then call local scenedrive } function InpAngleZ.SpinnerValue(new v) {spZ$=dec1$(v)} function InpAngleZ.ValidString(new &v$, &where) { call local clearInput call local commonValid, &angleZ } spX$ =dec1$(angleX) spY$ =dec1$(angleY) spZ$ =dec1$(angleZ) boolean skiponce function image1.click { if skiponce then skiponce=false else call local scenedrive } function Form1.enter { skiponce=true call local scenedrive } function image1.gotfocus { exit skiponce=true call local scenedrive } function image1.LostFocus { exit skiponce=false pushbutton=true call local scenedrive } boolean OnlyTitleMove module scenedrive { pushbutton~ if pushbutton then if scene_run>0 then OnlyTitleMove=false: thread scene_run restart: call local DisableRotation else if scene_run>0 then thread scene_run hold: OnlyTitleMove=true: call local EnableRotation end if } thread { Layer form1 { if clearplease then Gradient gColor:clearplease~ else release cursor 0, height-1 move random(scale.x), random(scale.y): draw to random(scale.x), random(scale.y), random(0,7) hold print part @(width-27), $(0,9),format$("{0:1:-8}°{1:1:-8}°{2:1:-8}°",angleX/10~,angleY/10~, angleZ/10~) print part time$(if(locale=1032->now, time("UTC")), locale, "hh:mm:ss tt ");if$(locale=1032->time$(),"UTC"); " press Esc to quit" rem print part time$(time("UTC"), locale, "hh:mm:ss tt");" UTC" refresh 2000 } } as clock interval 60 module resetSceneState { angleX = 0~ angleY = 0~ angleZ = 0~ method og "SetCameraPosition", 0~, 0~, -10~ method og "SetCameraRotation", 0~, 0~, 0~ } method og "ClearLights" method og "SetAmbient", 0.18~, 0.18~, 0.22~, 1~ method og "AddPointLight", 0~, 5~, 6~, 0.55~, 0.55~, 0.55~, 0~, 0~, 0~, 0.8~, 0.8~, 0.8~, 1~, 0.03~, 0~ method og "AddSpotLight", -8~, 6~, 8~, 1~, -0.6~, -1~, 25~, 12~, 1~, 0.85~, 0.7~, 0~, 0~, 0~, 1~, 1~, 1~, 1~, 0.02~, 0~ method og "AddSpotLight", 8~, 5~, 7~, -1~, -0.5~, -1~, 22~, 18~, 0.6~, 0.7~, 1~, 0~, 0~, 0~, 1~, 1~, 1~, 1~, 0.025~, 0~ method og "ApplyLights"
module sceneone { method og, "SetBackgroundColor", 0.2, 0.6, 0.9 method og, "SetBackgroundPictureUnder", form1, 0, titleheight method og, "BeginSceneEx" method og, "LightingOn" method og, "DrawBackgroundFullScreen", -25~ method og, "DrawCube", angleX/10~,angleY/10~, angleZ/10~,0~, 0~, 0~, 4~ method og, "DrawImage", number:=1, width:=10, height:=10, x:=0, y:=0, z:=-3 , angleX:=-30, angleY:=angleY/5~ method og, "DrawHudlineX20", 0,"first line", 1,1,1 method og, "DrawText3D", PosGreek, 3.2~, 1~, 0.28~, "VB6 - Ελληνικά", 1~, 1~, 1~, 0~, 0~, 0~ method og, "DrawText3D", -2.6~, -3.2~, 1~, 0.28~, "OpenGL Κείμενο", 1~, 0.8~, 0.2~, 0~, 0~, 0~ method og, "EndScene" } module titlemove { PosGreek+=0.2*d if PosGreek<-4.6 then d-! else.if PosGreek>2.6 then d-! } thread { static once as boolean if ReadNoFocus and not free then if once else one=true call local sceneone call local sceneone end if method og, "Present" else once=false if OnlyTitleMove then call local titlemove call local sceneone end if ' Layer form1 {refresh 25}
} as scene interval 30 Boolean bpass thread { if free then bpass=false else bpass=ReadNoFocus if bpass else call local titlemove spX$=dec1$(angleX+12~) spY$=dec1$(angleY+18~) spZ$=dec1$(angleZ) call local sceneone end if } as scene_run hold cls gradient 1, 5 move scale.x/2, scale.y/2 circle fill 4, scale.y/3 If ask("Hide the M2000 Console","Cube3d", "*Yes", "No")=1 then title "Cube3d", 0 method form1, "show" call local resetSceneState method form1, "Opacity", 180