So bear with me.
Fetching the Code
Picking up where we left off, our JavaScript runtime right now basically consists of a window.load event handler. This currently hacks a single cell into a single row in the content area. So our first milestone should be to replace that with correctly executing the init method and having the display reflect the desired layout.That's still a big goal.
First, let's just see if we can download the JSON code from the server. As far as I'm aware, even though this is static content (from the point of view of the web page), it isn't possible to load it using a <script> or <link> tag and recover the text from JavaScript (if this is possible, please let me know how). Instead, we're going to use the Fetch API. We're going to put this in at the top of our event handler, and, while we're about it, delete everything else that was there. The Fetch API returns a Promise, so we will attach a then action to it to:
- extract all the JSON into a repository "over here";
- execute the init method;
- render the main layout when we're done.
Step 1 is to clean out all the old code in the event handler and fetch the contents of /till-code:
window.addEventListener('load', function(ev) {
this.fetch("/till-code").then(resp => {
if (!resp.ok) {
console.log("Response err:", resp.status, resp.statusText)
return
}
resp.json().then(json => console.log(json));
});
})
CDP_FETCH_CODE:cdp-till/website/js/till-runtime.js
When I added the code to watch the website changing, I complained that it didn't seem to do a "hard" reload and just used the cached version of the JavaScript files. Now that it actually matters, I'm glad to report that everything is working correctly, and as I make changes to the JavaScript in VSCode, they are automatically reflected in the browser. Long may that continue!The json() method extracts the body and parses it as JSON, so by the time we are in the second "callback" we have an actual JavaScript object (not just JSON text), so we can get straight on with building up a repository. As I said, we're going to be using modules, so we'll put the repository in its own file and import it.
So Step 2 is to delegate parsing the JSON to a Repository:
import { Repository } from "./repository.js";
window.addEventListener('load', function(ev) {
this.fetch("/till-code").then(resp => {
if (!resp.ok) {
console.log("Response err:", resp.status, resp.statusText)
return
}
resp.json().then(json => {
var repo = new Repository(json);
});
});
})
CDP_JS_REPO:cdp-till/website/js/till-runtime.js
The Repository is responsible for going through all these items and creating Method and Layout objects:import { Method } from "./method.js";
import { Layout } from "./layout.js";
class Repository {
constructor(json) {
this.layouts = {};
this.methods = {};
for (var x of json) {
switch (x.EntryType) {
case "method": {
this.methods[x.Name] = new Method(x);
break;
}
case "layout": {
this.layouts[x.Name] = new Layout(x);
break;
}
default: {
console.log("what is an", x.EntryType);
break;
}
}
}
console.log(this.layouts);
console.log(this.methods);
}
}
export { Repository }
CDP_JS_REPO:cdp-till/website/js/repository.js
And we end up with one Layout and 9 Methods. So far, so good. I'm not showing Method and Layout themselves, because they are just stubs at this point. When we need them, we will complete them and I will show them.Continuing to push on a broad front and trying to get something to work, Step 3 sets up the global state and calls the init method:
import { Repository } from "./repository.js";
import { RuntimeState } from "./state.js";
window.addEventListener('load', function(ev) {
this.fetch("/till-code").then(resp => {
if (!resp.ok) {
console.log("Response err:", resp.status, resp.statusText)
return
}
resp.json().then(json => {
var repo = new Repository(json);
var state = new RuntimeState();
var init = repo.methods["init"];
init.execute(state);
console.log(state);
});
});
})
CDP_JS_INIT:cdp-till/website/js/till-runtime.js
The final line will show us how we're getting on. At the moment of course, it's just an empty object because it is simply a stub.Looking back at our sample code, this is the init method
init
enable all
disable NEXT
disable DONE
milks <- Oat Dairy Almond
drinks <- Tea Coffee Steamer Water
CDP_JS_INIT:cdp-till/samples/cafe.till
The idea here is that the enable and disable operations will make individual buttons active (or inactive). We'll come back to how that works when we handle the layout. The "assign" statements intend that the variables milks and drinks should be initialized (if necessary) and the values on the right hand side should be appended to them. We can do this.import { Assign } from "./assign.js";
class Method {
constructor(json) {
this.actions = [];
for (var a of json.Actions) {
switch (a.ActionName) {
case "assign": {
this.actions.push(new Assign(a));
break;
}
default: {
console.log("not handled", a.ActionName);
break;
}
}
}
console.log(this.actions);
}
execute(state) {
for (var a of this.actions) {
a.execute(state);
}
}
}
export { Method };
CDP_JS_ASSIGN:cdp-till/website/js/method.js
In the constructor, we attempt to build a list of actions by going through the JSON we've been given and building the appropriate action for each ActionName. For now, we are only handling assign. Then, when we want to execute the method, it can simply call execute on each of these actions in turn, passing it the same state.Assign itself follows the same pattern:
class Assign {
constructor(json) {
this.lineNo = json.LineNo;
this.dest = json.Dest;
this.append = json.Append;
}
execute(state) {
if (typeof(state[this.dest]) === 'undefined') {
state[this.dest] = [];
}
for (var x of this.append) {
state[this.dest].push(x);
}
}
}
export { Assign };
CDP_JS_ASSIGN:cdp-till/website/js/assign.js
In its execute method, it ensures that the dictionary has the desired entry, and then pushes all of the items on the right hand side of the assignment onto it. Note that the behaviour of assign is to append not to replace.Layout
Pushing on to the final step of round 1, we should be able to "display" the till layout to the user. In fact, we could probably have done this before. What we really need before we can do the laying out is the set of buttons we want to lay out. This just comes straight from the list of methods.I was about to write "we can tell these apart because of their EntryType" when I realized that, in fact, I had not set this correctly in the compiler. I've gone back and fixed that and now I can carry on. (See tag CDP_GO_BUTTONS.)
We can tell buttons apart from methods by their entry type. We want to create a method for each, but for the buttons we also want to add their names to a button dictionary. We'll return to what goes in the dictionary in a bit.
import { Method } from "./method.js";
import { Layout } from "./layout.js";
class Repository {
constructor(json) {
this.buttons = {};
this.layouts = {};
this.methods = {};
for (var x of json) {
switch (x.EntryType) {
case "button": {
this.methods[x.Name] = new Method(x);
this.buttons[x.Name] = {};
break;
}
case "method": {
this.methods[x.Name] = new Method(x);
break;
}
case "layout": {
this.layouts[x.Name] = new Layout(x);
break;
}
default: {
console.log("what is an", x.EntryType);
break;
}
}
}
}
}
export { Repository }
CDP_JS_BUTTONS:cdp-till/website/js/repository.js
Now let's turn to the laying out code itself. First time through, this obviously needs to come from the onload event:import { LayoutEngine } from "./layoutengine.js";
import { Repository } from "./repository.js";
import { RuntimeState } from "./state.js";
window.addEventListener('load', function(ev) {
this.fetch("/till-code").then(resp => {
if (!resp.ok) {
console.log("Response err:", resp.status, resp.statusText)
return
}
resp.json().then(json => {
var repo = new Repository(json);
var state = new RuntimeState(repo.buttons);
var init = repo.methods["init"];
init.execute(state);
var engine = new LayoutEngine(document);
engine.layout(repo.layouts["main"], state);
});
});
})
CDP_JS_LAYOUT_1:cdp-till/website/js/till-runtime.js
We are passing the buttons map from the repository to the state because we need it in layout and also because we will want to maintain "current state" about the buttons there. We then introduce a new class LayoutEngine, initialize it with the document (so that it can update the DOM, obviously) and then call layout, passing it the main layout (I mentioned when we were parsing the document that this was a hack; this is the corresponding portion of the hack) and the current state.This obviously requires us to actually complete the creation of the Layout from the JSON:
class LayoutRow {
constructor(json) {
this.lineNo = json.LineNo;
this.tiles = json.Tiles;
}
}
class Layout {
constructor(json) {
this.lineNo = json.LineNo;
this.rows = [];
for (var r of json.Rows) {
this.rows.push(new LayoutRow(r));
}
}
}
export { Layout }
CDP_JS_LAYOUT_1:cdp-till/website/js/layout.js
Which is really nothing more than "unpacking" it into a set of rows. It might be worth pointing out that I am totally ignoring the row "names" on the left hand side. They were really only ever there as a "syntactic device" to make the "assignments" look like all the other code.And we have a LayoutEngine which does the obvious initialization and with a stub in layout that enables me to check that everything is as it should be:
class LayoutEngine {
constructor(document) {
this.writeTo = document.getElementById("content-goes-here");
this.root = document.getElementById("root");
this.row = document.getElementById("row");
this.cell = document.getElementById("cell");
}
layout(layout, state) {
debugger;
}
}
export { LayoutEngine }
CDP_JS_LAYOUT_1:cdp-till/website/js/layoutengine.js
Implementing layout is another one of those trivial but messy functions that seem to plague this blog. We've already seen how this works in the initial hack we deleted earlier. Now we just need to wrap that code up in a double loop passing over the rows and cells.layout(layout, state) {
this.writeTo.innerHTML = '';
var iroot = this.root.content.cloneNode(true);
iroot = this.writeTo.appendChild(iroot.children[0]);
for (var r of layout.rows) {
var irow = this.row.content.cloneNode(true);
irow = iroot.appendChild(irow.children[0]);
for (var c of r.tiles) {
var icell = this.cell.content.cloneNode(true);
icell = irow.appendChild(icell.children[0]);
icell.className = "cell blue-cell";
if (!state.buttons[c]) {
icell.classList.add("blue-text");
}
var tc = icell.querySelector(".cell-text");
tc.appendChild(document.createTextNode(c));
}
}
}
CDP_JS_LAYOUT_2:cdp-till/website/js/layoutengine.js
To make the blank entries not show any text, I have added a test that there must be a button defined for the label in order for the text to be printed (in fact, it is there for CSS reasons, but in blue).This comes out like this (for full disclosure, I also had to make some CSS changes to make that work):

which, for a first cut, is none too bad. The obvious thing that stands out to me is that a lot of the buttons I thought were there (Steamer, Strong, Black) are not. Of course, this turns out to be the first discovered bug in a .till program: I referenced these in the layout but did not define them. A decent compiler would have picked up on this, but we deliberately didn't do any of that kind of checking in order to save time yesterday 🙂
So, now let's go back and add those and try again.

OK, that's what I had in mind.
Actions
Now, I think I may have stated that I view these as buttons. As such, I expect to be able to press them and have something happen. Specifically, I would expect that I can press a button and the "code" associated with it would execute and then the screen would redisplay.But let's start small and say that, as long as there is a button associated with the cell we create on the screen, we will add a click event handler for it. For now, it can just point out that it has been clicked.
layout(layout, state) {
this.writeTo.innerHTML = '';
var iroot = this.root.content.cloneNode(true);
iroot = this.writeTo.appendChild(iroot.children[0]);
for (var r of layout.rows) {
var irow = this.row.content.cloneNode(true);
irow = iroot.appendChild(irow.children[0]);
for (var c of r.tiles) {
var icell = this.cell.content.cloneNode(true);
icell = irow.appendChild(icell.children[0]);
icell.className = "cell blue-cell";
if (state.buttons[c]) {
this.addClick(icell, state.buttons[c]);
} else {
icell.classList.add("blue-text");
}
var tc = icell.querySelector(".cell-text");
tc.appendChild(document.createTextNode(c));
}
}
}
addClick(tile, buttonInfo) {
tile.addEventListener("click", ev => {
console.log("click on", buttonInfo);
});
}
CDP_CLICK_HANDLER:cdp-till/website/js/layoutengine.js
As an aside on JavaScript programming in general, it is my experience that I can almost never call addEventListener inline, but always need to call it from a function. This is to do with variable capture and the fact that the loop keeps spinning and updating the value of c so that all of the buttons end up doing whatever is appropriate for the last action. Of course, putting things in separate functions is good practice anyway, but since this has bitten me a number of times, I thought I'd mention it.Given everything we've done so far, it shouldn't be too hard to invoke the method associated with the button. The only problem is that we have the button info in the state, but we don't have a pointer to the code. Not a problem: we can easily go back and add that.
In the repository we can store it in the button state at the same time that we store it in the repository:
case "button": {
var m = new Method(x);
this.methods[x.Name] = m;
this.buttons[x.Name] = { methodCode: m };
break;
}
CDP_CLICK_METHOD:cdp-till/website/js/repository.js
Then when we initialize the state, we can copy it:class RuntimeState {
constructor(buttons) {
this.buttons = {}
for (var k of Object.keys(buttons)) {
this.buttons[k] = { methodCode: buttons[k].methodCode };
}
}
}
export { RuntimeState };
CDP_CLICK_METHOD:cdp-till/website/js/state.js
And then when the user clicks, we can call it:addClick(state, tile, buttonInfo) {
tile.addEventListener("click", ev => {
console.log("click on", buttonInfo);
buttonInfo.methodCode.execute(state);
console.log("state", state);
});
}
CDP_CLICK_METHOD:cdp-till/website/js/layoutengine.js
(The alert among you will have noticed we also passed state into this method.)And in the console window, we can see that the state is updating when we push buttons.
Nothing is changing in the window, however. There are two reasons for this: firstly, we're not changing anything that would make a difference to the window; and secondly, we aren't calling layout again. Let's fix that one first as it's easier.
Updating window layouts is notoriously difficult (consider, for example, redisplay code in Emacs). So we're not going to attempt this here. When we want to redisplay, we are just going to start over. While this may be sub-optimal, this whole thing is just a toy so we can build a debugger this afternoon or tomorrow, so I'm not going to worry about all kinds of error-prone code for a bit of optimization.
addClick(layout, state, tile, buttonInfo) {
tile.addEventListener("click", ev => {
buttonInfo.methodCode.execute(state);
this.layout(layout, state);
});
}
CDP_RELAYOUT:cdp-till/website/js/layoutengine.js
(The alert among you will have noticed we have now also passed layout into this method. There is a case to be made that "the current layout" should be a member of the state, and then we wouldn't need to do that.)Enabling and Disabling Buttons
We don't have a lot left to do in this runtime, but the thing that would make the biggest difference right now would be to allow buttons to be enabled and disabled. These are two of the actions we haven't implemented yet, and they should apply appropriate classes to the display to make the disabled buttons clearly inoperable. At the same time, disabled buttons won't have the event handler added to them.So, first let's try and create a new action in Method. Note that I'm going to just use one class for both enable and disable and just let it figure out what to do based on the action name:
constructor(json) {
this.lineNo = json.LineNo;
this.actions = [];
for (var a of json.Actions) {
switch (a.ActionName) {
case "assign": {
this.actions.push(new Assign(a));
break;
}
case "enable":
case "disable": {
this.actions.push(new Enable(a));
break;
}
default: {
console.log("not handled", a.ActionName);
break;
}
}
}
}
CDP_ENABLE:cdp-till/website/js/method.js
And then here is the class for doing that:class Enable {
constructor(json) {
this.lineNo = json.LineNo;
this.enabled = json.ActionName == 'enable';
this.tiles = json.Tiles;
}
execute(state) {
for (var t of this.tiles) {
if (t == "all") { // enable/disable all
for (var b of Object.keys(state.buttons)) {
this.set(state.buttons[b]);
}
} else {
var b = state.buttons[t];
debugger;
if (b) {
this.set(b);
}
}
}
}
set(b) {
if (this.enabled) {
delete b.disabled;
} else {
b.disabled = 'disabled';
}
}
}
export { Enable };
CDP_ENABLE:cdp-till/website/js/enable.js
The execute here is basically just a loop over all the "tile" arguments to enable or disable. If one of the arguments is all, then it goes through all the buttons and calls set. On the other hand, if the argument is not all, it tries to find the named button and set just that one. It doesn't attempt to point out the obvious, such as that not specifying any tiles does nothing, or that providing a list of tiles along with all in tautalogous. We don't have time for that (if we did, I would put it in a checking phase in the compiler).We now need to update the layout engine to look for this and change the layout accordingly.
for (var c of r.tiles) {
var icell = this.cell.content.cloneNode(true);
icell = irow.appendChild(icell.children[0]);
icell.className = "cell blue-cell";
var b = state.buttons[c];
if (b) {
if (b.disabled) {
icell.classList.add("disabled-tile");
} else {
this.addClick(layout, state, icell, state.buttons[c]);
}
} else {
icell.classList.add("blue-text");
}
var tc = icell.querySelector(".cell-text");
tc.appendChild(document.createTextNode(c));
}
CDP_LAYOUT_DISABLED:cdp-till/website/js/layoutengine.js
If the tile is marked as "disabled", the cell is given the additional class disabled-tile, and the event handler is not added.At the same time, I've changed "blank" cells to look like disabled ones in the CSS.
And then we have the following image when the till starts up:

And you can now click on the various tiles and things will happen. Clicking NEXT re-enables most of the tiles, and allows DONE to be selected. Clicking DONE takes you back to the starting position.
Style and Clear
We have three more actions to implement. We'll put submit off until the next section, but here we'll do style and clear together.style says that the current button should have a particular style (or styles) added to it. For our purposes, this means that the argument(s) are just to be interpreted as strings. clear is designed to clear out an array: as I've said, the "assign" operator (<-) always appends, so if you want assignment semantics, you need to call clear first.
OK, so we need to add handlers to the Method class:
constructor(json) {
this.lineNo = json.LineNo;
this.actions = [];
this.styles = [];
for (var a of json.Actions) {
switch (a.ActionName) {
case "assign": {
this.actions.push(new Assign(a));
break;
}
case "enable":
case "disable": {
this.actions.push(new Enable(a));
break;
}
case "clear": {
this.actions.push(new Clear(a));
break;
}
case "style": {
for (var s of a.Styles) {
this.styles.push(s);
}
break;
}
default: {
console.log("not handled", a.ActionName);
break;
}
}
}
}
CDP_STYLE_CLEAR:cdp-till/website/js/method.js
clear functions in much the same way as the previous two, creating and appending an action. style is different, because it is purely declarative and we want it to happen immediately. So we collect together a single list of all the styles attached to a button, right here, right now.We then copy these from the Method definition into the button state when we create the state along with the methodCode (yes, I know this isn't strictly necessary, since it is in the methodCode, but it seems clearer to me):
constructor(buttons) {
this.buttons = {}
for (var k of Object.keys(buttons)) {
this.buttons[k] = { methodCode: buttons[k].methodCode, styles: buttons[k].methodCode.styles };
}
}
CDP_STYLE_CLEAR:cdp-till/website/js/state.js
And then in the layout engine, we can just add those styles to the cell when we render it:for (var c of r.tiles) {
var icell = this.cell.content.cloneNode(true);
icell = irow.appendChild(icell.children[0]);
icell.className = "cell blue-cell";
var b = state.buttons[c];
if (b) {
if (b.styles) {
for (var s of b.styles) {
icell.classList.add(s);
}
}
if (b.disabled) {
icell.classList.add("disabled-tile");
} else {
this.addClick(layout, state, icell, state.buttons[c]);
}
} else {
icell.classList.add("blue-text");
}
var tc = icell.querySelector(".cell-text");
tc.appendChild(document.createTextNode(c));
}
CDP_STYLE_CLEAR:cdp-till/website/js/layoutengine.js
clear on the other hand, works in the normal way, and its execute method just deletes the key(s) from the state that it has been asked to clear:class Clear {
constructor(json) {
this.lineNo = json.LineNo;
this.vars = json.Vars;
}
execute(state) {
for (var v of this.vars) {
delete state[v];
}
}
}
export { Clear };
CDP_STYLE_CLEAR:cdp-till/website/js/clear.js
And that just leaves us with submitSubmit
The conceit for this application is that we are ordering drinks on a till in a cafe. We need, therefore, to be able to submit them somewhere. We have a "done" button that has submit as one of its instructions along with a variable. That variable should have a long list of noun/adj/adj/noun/adj/noun/noun in it, where each noun is followed by its applicable adjectives. We want to send this back to the server, where we expect it to be printed out (by implication on a device that somebody will then pick up and make the drinks).So we start off doing exactly what we have done so many times before in Method:
constructor(json) {
this.lineNo = json.LineNo;
this.actions = [];
this.styles = [];
for (var a of json.Actions) {
switch (a.ActionName) {
case "assign": {
this.actions.push(new Assign(a));
break;
}
case "enable":
case "disable": {
this.actions.push(new Enable(a));
break;
}
case "clear": {
this.actions.push(new Clear(a));
break;
}
case "style": {
for (var s of a.Styles) {
this.styles.push(s);
}
break;
}
case "submit": {
this.actions.push(new Submit(a));
break;
}
default: {
console.log("not handled", a.ActionName);
break;
}
}
}
}
CDP_SUBMIT_OUTLINE:cdp-till/website/js/method.js
And then we can create an outline of submit:class Submit {
constructor(json) {
this.lineNo = json.LineNo;
this.var = json.Var;
}
execute(state) {
console.log(state[this.var]);
}
}
export { Submit };
CDP_SUBMIT_OUTLINE:cdp-till/website/js/submit.js
And then we can test this and see what comes out:[ "noun", "adjs" ]Oh, that's not right. I don't know if I mentioned it to you (I obviously forgot to tell the computer), but when we do the "assignment", we're supposed to look and see if the state has a value for the specified key. If it does, we're supposed to expand that value into the result, not just copy the name. On the other hand, if the key is not there, we just assume it's a string. Yes, of course it's a hack. This whole thing is hacky, but as long as we can get to write a debugger for it, I'm happy.
So let's go and put that hack in.
execute(state) {
if (typeof(state[this.dest]) === 'undefined') {
state[this.dest] = [];
}
for (var x of this.append) {
if (state[x]) {
var a = state[x];
if (Array.isArray(a)) {
for (var c of a) {
state[this.dest].push(c);
}
} else {
state[this.dest].push(a);
}
} else {
state[this.dest].push(x);
}
}
}
CDP_HACK_VARS:cdp-till/website/js/assign.js
So, it still doesn't work perfectly, but it's pretty much how I expected, and it does at least send across the variables:['Coffee', 'adjs', 'Tea', 'Black']The thing is, if there aren't any adjectives selected, it still sends across the adjs because the key has not been found. I can fix this by updating clear to not delete the key, but to store an empty array and then adding clear to both noun and adjs in init.
execute(state) {
for (var v of this.vars) {
state[v] = [];
}
}
CDP_CLEAR_EMPTY:cdp-till/website/js/clear.js
['Coffee', 'Tea', 'Black']OK, I'm happy now.
Returning to submit, we can finish this off using the (misnamed?) Fetch API to POST the order to a mythical endpoint /order:
execute(state) {
var json = JSON.stringify(state[this.var]);
fetch("/order", { method: "POST", body: json }).then(resp => {
console.log("submitted order ... response =", resp.status, resp.statusText);
});
}
CDP_SUBMIT_FETCH:cdp-till/website/js/submit.js
As it stands, this doesn't actually "work" of course, because the endpoint isn't there:submitted order ... response = 404 Not FoundBut we can bash that out in five minutes:
func StartServer(addr string, repo compiler.Repository) {
handlers := http.NewServeMux()
index := NewFileHandler("website/index.html", "text/html")
handlers.Handle("/{$}", index)
handlers.Handle("/index.html", index)
favicon := NewFileHandler("website/favicon.ico", "image/x-icon")
handlers.Handle("/favicon.ico", favicon)
cssHandler := NewDirHandler("website/css", "text/css")
handlers.Handle("/css/{resource}", cssHandler)
jsHandler := NewDirHandler("website/js", "text/javascript")
handlers.Handle("/js/{resource}", jsHandler)
repoHandler := NewRepoHandler(repo, "application/json")
handlers.Handle("/till-code", repoHandler)
orderHandler := NewOrderHandler()
handlers.Handle("/order", orderHandler)
server := &http.Server{Addr: addr, Handler: handlers}
err := server.ListenAndServe()
if err != nil && !errors.Is(err, http.ErrServerClosed) {
fmt.Printf("error starting server: %s\n", err)
}
}
CDP_GO_ORDER_HANDLER:cdp-till/internal/web/server.go
And the OrderHandler is defined in handlers.go:type OrderHandler struct {
}
func (r *OrderHandler) ServeHTTP(resp http.ResponseWriter, req *http.Request) {
body, err := io.ReadAll(req.Body)
if err != nil {
panic(err)
}
var order []string
err = json.Unmarshal(body, &order)
if err != nil {
panic(err)
}
fmt.Println(" --- NEW ORDER ---")
for _, item := range order {
fmt.Printf(" %s\n", item)
}
}
CDP_GO_ORDER_HANDLER:cdp-till/internal/web/handlers.go
And can be created by a call to NewOrderHandler:func NewOrderHandler() http.Handler {
return &OrderHandler{}
}
CDP_GO_ORDER_HANDLER:cdp-till/internal/web/handlers.go
And thus we end up with this on the Go console:--- NEW ORDER ---All right.
Coffee
Tea
Steamer
Coffee
Strong
Black
Conclusion
Step by step, we have built up a runtime to go with our compiled language. We have demonstrated that it can work and as we've gone along, I've made a few changes to my original program. It's still a toy, and it's still hacky, but it is just about holding together.Now, if we only had a debugger 🙂
No comments:
Post a Comment