Binding
We have until now a glorified and uggly calculator. Right now is impossible to do one of the most basic operations: storing and loading values on variables. Let’s be modern and use type induction and use something like:
let a = 5; /* Constant */
let mutable b = 'Oi'; /* Variable*/
First edit our parse method to this:
parse(runtime) {
this.runtime = runtime;
while (this.nextToken !== null) {
this.parseStatement();
}
}
Then this new method:
parseStatement() {
const word = this.parseId();
if (word === 'let') {
const nextWord = this.parseId();
const mutable = nextWord === 'mutable';
const bindingName = mutable ? this.parseId() : nextWord;
this.match('=');
const value = this.parseMessage();
if (this.ctx[bindingName]) {
throw new Error('can not redeclare binding ' + bindingName)
}
this.ctx[bindingName] = {
type: 'binding',
valueType: this.getType(value),
value,
mutable,
}
this.match(';');
} else if (this.nextToken.type == '=') {
this.match('=');
const value = this.parseMessage();
const binding = this.ctx[word];
if (!binding) {
throw Error(`Variable ${word} not defined`);
}
const valueType = this.getType(value);
if (valueType !== binding.valueType) {
throw Error(`Assignment to ${word} expected ${binding.valueType.name}, got ${valueType.name}`);
}
binding.value = value;
this.match(';');
} else if (this.nextToken.type == '(') {
const functionDefinition = this.ctx[word];
if (!functionDefinition || functionDefinition.type !== 'function') {
throw new Error(`Function ${word} not defined`)
}
const parameters = this.parseParameters(functionDefinition.signature);
functionDefinition.definition.apply(this.runtime, parameters);
this.match(';');
} else {
throw Error('Invalid Statement ' + word);
}
}
Then our getType’s case ‘object’ to this:
case 'object':
if (value.type === 'type') {
return value;
} else if (value.type === 'binding') {
return value.valueType;
} else {
throw Error(`Invalid type ${value.type}`);
}
Then our parseValue’s case ‘id’ to this:
case 'id':
const name = this.parseId();
const definition = this.ctx[name];
if (!definition) {
throw new Error(`Name ${name} not declared`);
}
if (definition.type === 'type') {
return this.ctx[name];
}
if (definition.type === 'binding') {
return this.ctx[name].value;
}
And now we can execute code like this:
let a = 5;
writeln(a.toString());
let mutable s = 'Hi';
writeln(s);
s = 'Hello';
writeln(s);
A readln function
Now is time to add a readln function. We already have a place to store a value so it will be useful. The function itself is very easy to put:
exports.readln = {
type: 'function',
name: 'readln',
signature: [],
returns: 'void',
definition: function (text) {
return this.readln();
}
}
But we can’t call functions inside parameters, so we modify our parseValue’s “id” case to this:
const name = this.parseId();
const definition = this.ctx[name];
if (!definition) {
throw new Error(`Name ${name} not declared`);
}
if (this.nextToken.type === '(') {
if (definition.type === 'function') {
const parameters = this.parseParameters(definition.signature);
return definition.definition.apply(this.runtime, parameters);
}
throw new Error(`Expected function, got ${definition.type}`);
} else {
if (definition.type === 'type') {
return this.ctx[name];
}
if (definition.type === 'binding') {
return this.ctx[name].value;
}
throw new Error(`Expected type name or binding, got ${definition.type}`);
}
We need also to modify our interpreter’s runtime to add this function.
const runtime = {
/* ... */
readln() {
const buf = Buffer.alloc(1);
let result = '';
while (fs.readSync(0, buf, 0, 1, null) !== 0) {
const ch = buf.toString();
if (ch === '\n') {
return result;
}
if (ch === '\r') {
continue;
}
result += ch;
}
return result;
}
}
And that’s it. See my source code to compare with yours. And head to next lesson.