Zikzak - crummiest 8bit console/computer ever .. but I'm making it!


I meant to look into putting the Scoping rules into the "AST" (the element tree); as is, when you do a GOSUB, that handler does an enter- and exit-scope around the 'run element' business. Should be fine, but at the same time, the 'parsing/compiling to AST' phase could just insert an element for 'enter scope' and such. I thik theres implications there for if doing arbitrary goto and gosub about where it would land.

Theres also of course Closures and other fun things you can do with scopes, that my _very_ rudimentary scoping rules wouldn't handle. So far, a scope is just a nested symbol table.. nothing more. Its not really what it should be, but it should do the job for a toy language.

I also need to clean it up to make the interpreter and lexer into structs, so that if someone wanted, they could instantiate several interps at once. It uses a half dozen globals for storing state, so a cleaning would be nice. But it didnt' seem a priority, since its a toy, and there shouldn't likely be a need to spawn multiple instances simultaneously.
 
IMO something like FORTH would be a much better fit for such a computer (even though everybody used BASIC back then).
 
FORTH would mean a much simpler job parsing it, but IMO a lot of the time FORTH is so low level you may as well code in assembler anyway.

Regarding your toy language scoping idea, I think it's a neat concept. As I said previously, the GLOBAL keyword (or whatever you call it eventually) is going to be pretty important to making it usable for anything that does work but doesn't immediately output anything. I wonder how it'll work in nested routines - say your first subroutine doesn't declare any globals, but something it calls does declare some. Would changes to those be accessible to changes from all contexts? I think so. More concerning is whether changes in the first level of subroutine get picked up in the second level when it declares a global?

It could probably all be made to work by copying globals back up the tree before jumping back after a RETURN instruction, but it perhaps still needs a little thought.
 
Forth is pretty interesting really, but not what I wanted to go for here (not the kind of 'lulz' I wanted :)

for the GLOBAL keyword thing, it wouldn't "nest"; ie: By doing a GLOBAL in one procedure, it woudln't imply that if another subroutine is called that if it tried to use a var, that that referefe would also be global. That proc would also have to declare it global if it wanted.

Yes, I'm aware I just used procedure and subroutine interchangably, which is probably wrong, so I should decide what nomenclature I'm using :)

The way I've done it so far (buggily, anyway :) is .. The assignment operator (=) either applies an assignment to an existing var or creates a var, at the current scope. Other operators tend to look at current-or-global scope (preference to current), but assignment is local. I'm not sure I like the way that 'feels', but will have to see how it goes (if I keep at it :)

ex:

LET A = 1
GOSUB Someproc
EXIT

PROCEDURE Someproc
# GLOBAL A
LET A = 2
ENDPROC

(you can use "END", ENDIF, ENDPROC, etc, interchangably; they're all just 'END' right now, but with multiple names)

In this case (with comment there), A is 1 i global, and 2 in the procedure.

But if you uncomment the global, it now declares a local variable A, which is in fact a reference to the global A. Then the LET A=2 does its usual local lookup, and applies the change. It doesn't care that the local scope is pointing to global or local, its a local name, that points to a global value.

The way it works is ..

The progrma starts at global scope 0, and the scopes are just a stack. Every time you enter a procedure, it advances the scope pointer. Scope lookups can either by 'current and global' check, or 'current only' or 'global only', depending what the code needs to do. So as you can see.. when in a proc, it will check its own locals, or global .. it won't check the up-a-level scopes version of the variable .. its out of scope.

ie:

LET A = 1
gosub DOIT
..
PROCEDURE DOIT
GLOBAL A # bind this procs A to point to the value of global A
LET A = 2 # this now changes the global A to value 1
Gosub ALSODOIT
END

PROCEDURE ALSODOIT
LET A = 3 # this declares a new local A to be value 3; global A is still 2
END

So, the safety is.. any proc will by default create only local variables; ay var used that isn't declared, must be a global to be found. But if you want, you can force a value to refer to a global, so that you can update it.

Its clunky, like this, but I thought it woudl let a procedure safely declare and use its own values, without breaking random globals in use (ie: in case you do an INCLUDE type keyword ..)

It might be better to make it so _all_ lookups are LOCAL-only, unless explicitly declared as Global; this is trivially done in my codebase by changing a flag to the lookup routine, and seems a better approach.

ie: Very safe.. all values are proc-local only, unless you declare it to go local; this is consistent .. reads and writes both need local declaration to work at all, or a global declaration to go global; no 'implied global reads'.

TCL language does some of this 'global reads' busiess, and it always made me crazy, since your procedure could break just by someone making some globals....

I've obviously not put much thought into language design here :)

jeff
 
I don't really mind you using procedure and subroutine interchangeably here - I'd be surprised if I've managed to be anywhere near consistent here. It you using a GOSUB to call a PROCEDURE that puts my nose out of joint, but I guess that's my problem, and it's your language!

The place I think you need 'nested' GLOBALS is in something like this:

LET A=1
GOSUB foo
print A: REM A=3

ROUTINE foo
LET A=2
GOSUB bar
PRINT A: REM A=3
END

ROUTINE bar
GLOBAL A
LET A=3
END

i.e. when the interpreter hits bar's END statement, it needs to copy the value of its global variables (just A in this case) into foo and main's variable spaces.

Also, ending a routine with ENDIF - erk!! Actually, having an ENDIF - does that mean you support mutliline IF statements already?
 
IF statements are multiline, yeah; I don't have ELSE handled yet, but shouldn't be hard to add it. WHILE, IF, PROCEDURE are all multiline things. (Really, there is an element type 'sequence', which is just a linked list of other elements; so whenever I want something to contain more code, it just recurses into another parser; the parser returns a Sequence. So, it is trivial as you can see .. when it sees IF, it fires up another parser, and just assigns tehe result (if ay) to the IF. The language design, such as it is, was to make my life easy :)

Doing IF .. ELSE IF .. ELSE .. ENDIF should be pretty easy, I think.

On IF, it spawns a parser and captures the subcode, which finishes when an END is discovered. (if there is an IF or WHILE inside of that, there is another parser spawned, so it now expects multiple ENDs, etc.. so it all works out.)
So, make it also end on an ELSE (say), so this would now work:

IF ..
...
ELSE
..
ENDIF

ie: ELSE implies stop, store, spawn another parser for false, and go.

Even this should work..

IF..
..
ELSE IF ...
..
ENDIF

Since ELSE IF is really just ELSE (now the false path), and IF .... ENDIF stored within. So, the parser needs no extra intelligence to handle it.


As you could anticipate, I bet the problem is if you do a WHILE ... IF ... END, but that should work out just fine.

It would be pretty easy to handle ENDIF separately to ENDPROC and ENDWHILE etc, but so far I have not needed it, so they're just aliases.

....

I rather agree with the GOSUB and PROC thing of yours; I'm tempted to make it GOSUB and SUBROUTINE instead (a named subroutine, since I'm avoiding line numbers.) This then leaves FUNCTION as a thing that is an inline-procedure that has a return value and an argument list, and a PROCEDURE as a named subroutine that takes paramaters (GOPROC to invoke?) ..

...

As to your global thing .. I'm leaning more to the idea I posted at the end of the screed above; that, in light of the possible addition of an INCLUDE type keyword that inhales another file (say), that PROCs and SUBroutines should be able to assume some safety; their vars shoudl just work, in isolation. So, ALL assignment and interpretation of a var should only default to the current scope, unless a GLOBAL keyword has bound that var instead to a global (scope 0).. which could already exist (same name), or not (declare a new global from within a subroutine.)

So, this way, a proc is safe from collision by random naming of outside code, and also lets you modify the global space i a controlled fashion.

Of course, this means the only communication betwee scopes is globals, but thats how BASIC worked wasn't it? If this keeps going, could add scope-PIPEs, or Pointers, etc, so data can be passed easier, but BLEH! Paramaters for procs should be enough :)

jeff
 
Yes, I considered using SUBROUTINE instead of ROUTINE in my example above. It's a bit longer, but it has full symmetry with GOSUB and a new ENDSUB keyword, which is nice.

Perhaps I need a slightly more concrete example to show why GLOBAL keywords do not just need to affect the base variable store, but also any currently active ones:

A='foo'
GOSUB PRINTROT13A

SUBROUTINE PRINTROT13A
FOR I=0 to LEN(A)-1
C=SUBSTR(A,I,I+1): REM Single character at index I
GOSUB ROT13C
PRINT C,
NEXT
ENDSUB

SUBROUTINE ROT13C
GLOBAL C
IF ORD(C)<=ORD('Z') AND ORD(C)>=ORD('A') THEN
REM Uppercase
C=CHR((ORD(C)-ORD('A')+13) % 26 + ORD('A'))
ENDIF
IF ORD(C)<=ORD('z') AND ORD(C)>=ORD('a') THEN
REM Lower case
C=CHR((ORD(C)-ORD('a')+13) % 26 + ORD('a'))
ENDIF
ENDSUB

In other words, GLOBAL affecting the calling subroutine too allows for subroutines to call their own subroutines and act on the result. It strikes me that since we don't have to worry about concurrency here, just delay that work until the ENDSUB, and avoid having to copy it to AtoORD's varspace repeatedly, when most of those results won't be used.

Old-school BASIC didn't used to have scopes, in my experience. Primitive BASICs only had 26 variables or so, A-Z and the same values for each were used in all contexts. Mid 80s BASICs allowed for longer variable names, but they were all still global. I approve of your scoping your variables though, cos it's a cute idea that could make the language not quite so clutzy in real use, as you say.

Who knows, if you make a Zikzak env emulator, or I buy and build one, I might be up for making a few diversions for it, and having a reasonably specced BASIC as a launch environment like in the old days might be more maintainable than handling everything in assembler.

P.S. I can't remember what the BASIC inverse of CHR() was, so I'm borrowing a pythonism in that code there, beware.

Edit: Swapped out the dummy example for something slightly more concrete.
 
Last edited:
This new(ish) auto merging of double posts needs a double-like function!
[doublepost=1466646194,1466645113][/doublepost]Okay, just cloned and built. Had to eliminate the two dots before the printf on line 371 of element_op.c - not sure what they're meant to do, but my compiler barfed at them. Judging by those printfs it's not possible to run a non debug session yet, and it took a bit of divine inspiration to figure out how to run programs, but it's an alpha version so that's fair enough.
 
Consult the README :)

Checked in a fix for the .. typo; that was meant to be a // commenting out of the line, and I was/am falling asleep at the keyboard :O Sorry about that.

./sbrun 0 ./testcode/foo.sb
-> will run with almost no debug-junk on the screen

./sbrun 1 ./foo.sb
--> will show some higher level commentary/reporting

./sbrun 2 ./foo.sb
--> will show an enormous dump; ie: the line in, the tokens and parse/maching and resulting listing; handy for streaming out and then looking what-the-hell-its-doing

jeff
[doublepost=1466646529,1466646490][/doublepost]/archive/devo/projects/skeebasic> ./sbrun 0 ./testcode/let_while_print.sb
hello - #0
hello - #1
hello - #2
hello - #3
hello - #4
5
 
RL has been far too busy lately, not getting much fun stuff done. But today, took an hour and did some leatherworking .. learnt a new useless skill! I picked up archery as a hobby a year or two back, so wanted to make a new vambrace (wristguard that goes all the way around the arm) .. didn't take much time at all, and turned out pretty good. So, huh, making stuff.. fun :)

https://dl.dropboxusercontent.com/u...Archery vambrace/Photo Jul 01, 2 05 41 PM.jpg

jeff
 
Possibly of interest .. I just picked up a piece of astoundingly expensive gear in its day, that is now .. cheapish. Logic Analyzers are notoriously expensive for the 'real ones' - a Salae Logic is a pretty inexpensive USB based LA, with a bytes worth of pins on it. But the industrial strength LAs with 32 or 64 or 100+ pins are thousands of dollars .. often into the $20,000USD+ sort of scary range. I've been keeping an eye on the local trades, ebay, etc, for various HP and Tektronix LAs for some time, and once in awhile you see a deal. Its stuff that is fun to play with, and possibly really useful, so worth keeping an eye out. Well, I ended up spying a Tektronix one for next to nothing - a TLA 704 mainframe (has slots in it for various devices, such as oscciliscope, LA, and others, though mostly for LA), with a couple LA slots in it. So mine came with 2x LA, and each LA is 136pin. So I have a stupid number of pins I can monitor at once!

Modern LAs do awesome live decoding of stuff like serial, I2C, SPI, etc and so on; but they cost a mint. The older ones had a lot of options you could buy to add stuff like that, but not much built i (these predate I2C and SPI..) ... mind you, ay halfway recent oscilliscope can decode those anyway, if you have the channels (I do.)

(And what is an LA? A oscilliscope is good for measure voltage level over time, at 2 or 4 or so channels.. most hobbyists will have 2 channel, and if you're lucky, 4 channel; so 2 or 4 signqals at once. This limits the protocols you can deal with simultaneously, quite a bit. A LA doesn't deal with voltage levels (analog) like an o-scope does, but will filter them down to logic 0 or 1, and manage them that way.. times a lot of channels. So an o-scope will have 2 or 4, and an LA will have 32+ ...

So, with my LA, I can latch onto (say) all the address line pins, and the data pins, and various clocks, and scan them all, at once. Nice. The cool thing about LAs, is you can gorup the pins and label them as say 'address bus' or whatever, and once you have everything tuned up, you can then start doing some of that magic decoding business. So while it doesn't auto decode much, you can train it to. It also runs Win98 (how awesome is that, including Solitaire..), so you can have it record the scans to the disk, and run your own code to work through it, and script thigns into the UI if you really want to. Pentium CPU for the win :)

Anyway, hooking it up to Zikzak, I've been able to get it saying things like showing over time when the CPU is reading or writing to the address bus, or even cooler .. identify a trigger and pi sequence that tells us when itws writing GPU mode commands to the GPU, and then watching for the address bus read/writes to try and identify what actual commands are flowing into the GPU .. all in real time (or pseudo-real-time). Totally nifty.

So this is sort of like how real engineers work eh? Cool stuff.

I'm not sure yet what my next step is .. I'm designing a variable bench power supply right now for lulz (I have a big surplus toroidal transformer to use, and its a fun project) despite already having a few good bench power supplies; next up though, need to make another computer, or a handheld zikzak, or something sick ...

jeff
 
Nice. Presumably you still need an o'scope to check that your lines are not unbalanced/unterminated/generally a bit crappy. But once you've got lines that basically work, an LA is what you need to find out what they're doing. Back to the o'scope when you try overclocking and seeing how stable lines are though, I guess.
 
Back
Top