{{meta {load_files: ["code/chapter/12_language.js"], zip: "node/html"}}}
{{quote {author: "Hal Abelson and Gerald Sussman", title: "Structure and Interpretation of Computer Programs", chapter: true}
El evaluador, que determina el significado de las expresiones en un lenguaje de programación, es solo otro programa.
quote}}
{{index "Abelson, Hal", "Sussman, Gerald", SICP, "project chapter"}}
Construir tu propio ((lenguaje de programación)) es sorprendentemente fácil (siempre y cuando no apuntes demasiado alto) y muy esclarecedor.
Lo principal que quiero mostrar en este capítulo es que no hay ((magia)) involucrada en la construcción de tu propio lenguaje. A menudo he sentido que algunos inventos humanos eran tan inmensamente inteligentes y complicados que nunca podría llegar a entenderlos. Pero con un poco de lectura y experimentación, a menudo resultan ser bastante mundanos.
{{index "Egg language"}}
Construiremos un lenguaje de programación llamado Egg. Será un lenguaje pequeño y simple—pero lo suficientemente poderoso como para expresar cualquier computación que puedes pensar. Permitirá una ((abstracción)) simple basada en ((funcion))es.
{{id parsing}}
{{index parsing, validation}}
La parte más visible de un lenguaje de programación es su ((sintaxis)), o notación. Un analizador es un programa que lee una pieza de texto y produce una estructura de datos que refleja la estructura del programa contenido en ese texto. Si el texto no forma un programa válido, el analizador debe señalar el error.
{{index "special form", [function, application]}}
Nuestro lenguaje tendrá una sintaxis simple y uniforme. Todo en Egg
es una ((expresión)). Una expresión puede ser el nombre de una vinculación (binding), un
número, una cadena de texto o una aplicación. Las aplicaciones son usadas para
llamadas de función pero también para constructos como if
o while
.
{{index "double-quote character", parsing, [escaping, "in strings"]}}
Para mantener el analizador simple, las cadenas en Egg no soportarán nada parecido a escapes de barra invertida. Una cadena es simplemente una secuencia de caracteres que no sean comillas dobles, envueltas en comillas dobles. Un número es un secuencia de dígitos. Los nombres de vinculaciones pueden consistir en cualquier carácter no que sea un ((espacio en blanco)) y eso no tiene un significado especial en la sintaxis.
{{index "comma character"}}
Las aplicaciones se escriben tal y como están en JavaScript, poniendo ((paréntesis)) después de una expresión y teniendo cualquier cantidad de ((argumento))s entre esos paréntesis, separados por comas.
hacer(definir(x, 10),
si(>(x, 5),
imprimir("grande"),
imprimir("pequeño")))
{{index block}}
La ((uniformidad)) del ((lenguaje Egg)) significa que las cosas que son
((operador))es en JavaScript (como >
) son vinculaciones normales en este
lenguaje, aplicadas como cualquier ((función)). Y dado que la
((sintaxis)) no tiene un concepto de bloque, necesitamos una construcción
hacer
para representar el hecho de realizar múltiples cosas en secuencia.
{{index "type property", parsing}}
La ((estructura de datos)) que el analizador usará para describir un programa
consta de objetos de ((expresión)), cada uno de los cuales tiene una
propiedad tipo
que indica el tipo de expresión que este es y otras propiedades
que describen su contenido.
{{index identifier}}
Las expresiones de tipo "valor"
representan strings o números literales.
Su propiedad valor
contiene el string o valor numérico que estos
representan. Las expresiones de tipo "palabra"
se usan para identificadores
(nombres). Dichos objetos tienen una propiedad nombre
que contienen el
nombre del identificador como un string. Finalmente, las expresiones "aplicar"
representan aplicaciones. Tienen una propiedad operador
que se refiere
a la expresión que está siendo aplicada, y una propiedad argumentos
que
contiene un array de expresiones de argumentos.
La parte >(x, 5)
del programa anterior se representaría de esta manera:
{
tipo: "aplicar",
operador: {tipo: "palabra", nombre: ">"},
argumentos: [
{tipo: "palabra", nombre: "x"},
{tipo: "valor", valor: 5}
]
}
{{indexsee "abstract syntax tree", "syntax tree"}}
Tal ((estructura de datos)) se llama ((árbol de sintaxis)). Si tu imaginas los objetos como puntos y los enlaces entre ellos como líneas entre esos puntos, tiene una forma similar a un ((árbol)). El hecho de que las expresiones contienen otras expresiones, que a su vez pueden contener más expresiones, es similar a la forma en que las ramas de los árboles se dividen y dividen una y otra vez.
{{figure {url: "img/syntax_tree.svg", alt: "The structure of a syntax tree",width: "5cm"}}}
{{index parsing}}
Compara esto con el analizador que escribimos para el formato de archivo de configuración en el Capítulo 9, que tenía una estructura simple: dividía la entrada en líneas y manejaba esas líneas una a la vez. Habían solo unas pocas formas simples que se le permitía tener a una línea.
{{index recursion, [nesting, "of expressions"]}}
Aquí debemos encontrar un enfoque diferente. Las expresiones no están separadas en líneas, y tienen una estructura recursiva. Las expresiones de aplicaciones contienen otras expresiones.
{{index elegance}}
Afortunadamente, este problema se puede resolver muy bien escribiendo una función analizadora que es recursiva en una manera que refleje la naturaleza recursiva del lenguaje.
{{index "parseExpression function", "syntax tree"}}
Definimos una función analizarExpresion
, que toma un string como entrada
y retorna un objeto que contiene la estructura de datos para la expresión
al comienzo del string, junto con la parte del string que queda
después de analizar esta expresión Al analizar subexpresiones (el
argumento para una aplicación, por ejemplo), esta función puede ser llamada
de nuevo, produciendo la expresión del argumento, así como al texto que
permanece. A su vez, este texto puede contener más argumentos o puede ser el
paréntesis de cierre que finaliza la lista de argumentos.
Esta es la primera parte del analizador:
function analizarExpresion(programa) {
programa = saltarEspacio(programa);
let emparejamiento, expresion;
if (emparejamiento = /^"([^"]*)"/.exec(programa)) {
expresion = {tipo: "valor", valor: emparejamiento[1]};
} else if (emparejamiento = /^\d+\b/.exec(programa)) {
expresion = {tipo: "valor", valor: Number(emparejamiento[0])};
} else if (emparejamiento = /^[^\s(),"]+/.exec(programa)) {
expresion = {tipo: "palabra", nombre: emparejamiento[0]};
} else {
throw new SyntaxError("Sintaxis inesperada: " + programa);
}
return aplicarAnalisis(expresion, programa.slice(emparejamiento[0].length));
}
function saltarEspacio(string) {
let primero = string.search(/\S/);
if (primero == -1) return "";
return string.slice(primero);
}
{{index "skipSpace function"}}
Dado que Egg, al igual que JavaScript, permite cualquier cantidad de ((espacios
en blanco)) entre sus elementos, tenemos que remover repetidamente los espacios
en blanco del inicio del string del programa. Para esto nos ayuda la función
saltarEspacio
.
{{index "literal expression", "SyntaxError type"}}
Después de saltar cualquier espacio en blanco, analizarExpresion
usa tres
((expresiones regular))es para detectar los tres elementos atómicos que Egg
soporta: strings, números y palabras. El analizador construye un
tipo diferente de estructura de datos dependiendo de cuál coincida. Si
la entrada no coincide con ninguna de estas tres formas, la expresión no
es válida, y el analizador arroja un error. Usamos SyntaxError
en lugar de Error
como constructor de excepción, el cual es otro
tipo de error estándar, dado que es un poco más específico—también es el
tipo de error lanzado cuando se intenta ejecutar un programa de
JavaScript no válido.
{{index "parseApply function"}}
Luego cortamos la parte que coincidió del string del programa y
pasamos eso, junto con el objeto para la expresión, a aplicarAnalisis
,
el cual verifica si la expresión es una aplicación. Si es así,
analiza una lista de los argumentos entre paréntesis.
function aplicarAnalisis(expresion, programa) {
programa = saltarEspacio(programa);
if (programa[0] != "(") {
return {expresion: expresion, resto: programa};
}
programa = saltarEspacio(programa.slice(1));
expresion = {tipo: "aplicar", operador: expresion, argumentos: []};
while (programa[0] != ")") {
let argumento = analizarExpresion(programa);
expresion.argumentos.push(argumento.expresion);
programa = saltarEspacio(argumento.resto);
if (programa[0] == ",") {
programa = saltarEspacio(programa.slice(1));
} else if (programa[0] != ")") {
throw new SyntaxError("Experaba ',' o ')'");
}
}
return aplicarAnalisis(expresion, programa.slice(1));
}
{{index parsing}}
Si el siguiente carácter en el programa no es un paréntesis de apertura,
esto no es una aplicación, y aplicarAnalisis
retorna la expresión que
se le dio.
{{index recursion}}
De lo contrario, salta el paréntesis de apertura y crea el objeto de
((árbol)) de sintaxis para esta expresión de aplicación. Entonces, recursivamente
llama a analizarExpresion
para analizar cada argumento hasta que se encuentre
el paréntesis de cierre. La recursión es indirecta, a través de aplicarAnalisis
y analizarExpresion
llamando una a la otra.
Dado que una expresión de aplicación puede ser aplicada a sí misma (como en
multiplicador(2)(1)
), aplicarAnalisis
debe, después de haber analizado una
aplicación, llamarse asi misma de nuevo para verificar si otro par de
paréntesis sigue a continuación.
{{index "syntax tree", "Egg language", "parse function"}}
Esto es todo lo que necesitamos para analizar Egg. Envolvemos esto en una
conveniente función analizar
que verifica que ha llegado al final del string
de entrada después de analizar la expresión (un programa Egg es una sola
expresión), y eso nos da la estructura de datos del programa.
function analizar(programa) {
let {expresion, resto} = analizarExpresion(programa);
if (saltarEspacio(resto).length > 0) {
throw new SyntaxError("Texto inesperado despues de programa");
}
return expresion;
}
console.log(analizar("+(a, 10)"));
// → {tipo: "aplicar",
// operador: {tipo: "palabra", nombre: "+"},
// argumentos: [{tipo: "palabra", nombre: "a"},
// {tipo: "valor", valor: 10}]}
{{index "error message"}}
¡Funciona! No nos da información muy útil cuando falla y no almacena la línea y la columna en que comienza cada expresión, lo que podría ser útil al informar errores más tarde, pero es lo suficientemente bueno para nuestros propósitos.
{{index "evaluar función", evaluación, interpretación, "árbol sintáctico", "lenguaje de Egg"}}
¿Qué poddemos hacer son el árrbol sintáctico de un programa? ¡Correrlo, por supuesto! Y eso es lo que el evaluador hace. Le das un árbol sintáctico y un objeto de ámbito que asocie nombres con valores, y evaluará la expresión que el árbol representa y regresará el valor que el árbol produce.
const specialForms = Object.create(null);
function evaluate(expresion, scope) {
if (expresion.type == "value") {
return expresion.value;
} else if (expresion.type == "word") {
if (expresion.name in scope) {
return scope[expresion.name];
} else {
throw new ReferenceError(
`Undefined binding: ${expresion.name}`);
}
} else if (expresion.type == "apply") {
let {operator, args} = expresion;
if (operator.type == "word" &&
operator.name in specialForms) {
return specialForms[operator.name](expresion.args, scope);
} else {
let op = evaluate(operator, scope);
if (typeof op == "function") {
return op(...args.map(arg => evaluate(arg, scope)));
} else {
throw new TypeError("Applying a non-function.");
}
}
}
}
{{index "expresión literal", ámbito}}
El evaluador tiene código para cada uno de los tipos de ((exprresión)). Un
expresión literal de un valor produce este valor. (Por ejemplo, la expresión 100
evalúa
al número 100.) Para un vínculo de valor, debemos verificar si está definido en el ámbito,
y si lo está, traer el valor vinculado.
{{index [función, aplicación]}}
La apliación implica más cosas. Si son una ((forma especial)), como
if
, no evaluamos nada y pasamos las expresiones argumento, junto con el
ámbito, a la función que maneja esta forma. Si es una llamada normal, evaluamos el operador,
verificamos que sea una función, y la llamamos con los argumentos evaluados.
Usamos valores función planos de JavaScript para representar los valores función de Egg.
Regresaremos a esto más tardde, cuando forma especial llamada
fun
está definida.
{{index legibilidad, "evaluar función", recursión, parseo}}
La estructura recursiva de evaluate
se parece a la estructura del parser, y
ambos reflejan la estructura del lenguaje mismo. También sería posible
integrar el parser con el evaluador y evaluar durante el parseo,
pero separarlos de esta manera hace el programa más claro.
{{index "lenguaje Egg", interpretación}}
Esto es todo lo que se necesita para interpretar Egg. Es así de simple. Pero si no definimos unas cuantas formas especiales y agregamos algunos valores útiles para el ((entorno)), todavía no podemos hacer mucho con el lenguaje.
{{index "formas especiales", "objeto specialForms"}}
El objeto specialForms
es usado para definir una sintaxis especial en
Egg. Asocia palabras con funciones que evalúan estas formas. Ahora mismo está
vacío. Agreguemos if
.
specialForms.if = (args, scope) => {
if (args.length != 3) {
throw new SyntaxError("Wrong number of args to if");
} else if (evaluate(args[0], scope) !== false) {
return evaluate(args[1], scope);
} else {
return evaluate(args[2], scope);
}
};
{{index "ejecución condicional", "operador ternario", "operador ?:", "operador condicional"}}
El constructo if
de Egg espera exactamente tres arrgumentos. Evaluará el primero,
y si el resultado no es false
, evaluará el segundo. En otro caso, el
tercero será evaluado. Esta forma if
es más parecidda al operaddor
ternario ?:
que al if
if de JavaScript. Es una expresión, no una
sentencia, y produce un valor: el resultado del segundo o tercer argumento.
{{index Booleano}}
Egg también se diferencia de JavaScript en como maneja el valor de la
condición para if
. No tratará cosas como cero o la cadena vacía como
falso, solamente el valor exacto false
.
{{index "evaluación en corto circuito"}}
La razón por la que necesitamos representar if
como una forma especial,
en vez de como una función regular, es que todos los argumentos de las
funciones son evaluados antes de que la función sea llamada, mientras
que el if
debe evaluar solo el segundo o el tercer argumento,
dependiendo del valor del primero.
La forma while
es similar.
specialForms.while = (args, scope) => {
if (args.length != 2) {
throw new SyntaxError("Wrong number of args to while");
}
while (evaluate(args[0], scope) !== false) {
evaluate(args[1], scope);
}
// Since undefined does not exist in Egg, we return false,
// for lack of a meaningful result.
return false;
};
Otro bloque de construcción principal es do
, que ejecuta todos sus argumentos
de arriba hacia abajo. Su valos es el valorr producido por el último
argumento.
specialForms.do = (args, scope) => {
let value = false;
for (let arg of args) {
value = evaluate(arg, scope);
}
return value;
};
{{index "operador ="}}
Para ser capaces de crear ((asignaciones)) y darle nuevos valores,
además necesitamos crear una forma llamada define
. Espera una palabra
como prime argumento y una expresión producciendo un valor para asignar
a esa palabra como segundo argumento. Ya que define
, como todo,
es una expresión, debe regresar un valor. Haremos que regrese el valor que
fue asignado (justo como el operador =
de JavaScript).
specialForms.define = (args, scope) => {
if (args.length != 2 || args[0].type != "word") {
throw new SyntaxError("Incorrect use of define");
}
let value = evaluate(args[1], scope);
scope[args[0].name] = value;
return value;
};
{{index "lenguaje Egg", "evaluar función"}}
El ((ámbito)) aceptado por evaluate
es un objeto con propiedades
de las que sus nombres corresponden a los nombres de las vinculaciones en el ámbito
y sus valores corresnponden a los valores de esos vínculos. Ahora definamos un objeto
para representar el ((ámbito global)).
Para poder usar el constructo if
que acabamos de definir, necesitamos acceder
a valores ((Booleano))s. Como solo hay dos valores Booleanos, no necesitamos una sintaxis
especial. Simplemente asociaremos dos nombres a los valores true
y false
y usaremos estos nombres.
const topScope = Object.create(null);
topScope.true = true;
topScope.false = false;
Podemos evaluar una expresión simple que niegue un Booleano.
let prog = parse(`if(true, false, true)`);
console.log(evaluate(prog, topScope));
// → false
{{index aritmética, "constructor de Función"}}
Para proveer la ((aritmética)) y ((operador))es de ((comparación)),
necesitamos además añadir algunas funciones al ((ámbito)). Para mantener
el código peequeño, usaremos Function
para sintetizar varias
funciones de operador en un ciclo, en vez de definirlas individualmente.
for (let op of ["+", "-", "*", "/", "==", "<", ">"]) {
topScope[op] = Function("a, b", `return a ${op} b;`);
}
Tener una manera de ((devolver)) valores tambbién es muy útil,
así que envolveremos console.log
en una función y la llamaremos
print
.
topScope.print = value => {
console.log(value);
return value;
};
{{index parseo, "función run"}}
Esto nos da suficientes herramientas elementales para escribir programas simples. La siguiente función provee una forma conveniente de leer un programa y correrlo en un ámbito nuevo.
function run(program) {
return evaluate(parse(program), Object.create(topScope));
}
{{index "función Object.create", prototipo}}
Usareomos las cadenas de prototipos de los objetos para representar ámbitos anidados, para que el programa pueda añadir asignaciones a su ámbito local sin cambiar el ámbito superior.
run(`
do(define(total, 0),
define(count, 1),
while(<(count, 11),
do(define(total, +(total, count)),
define(count, +(count, 1)))),
print(total))
`);
// → 55
{{index "ejemplo de suma", "lenguaje Egg"}}
Este es el programa que hemos visto varias veces antes, que calcula la suma de los números del 1 a 10, expresado en Egg. Es claramente más feo que su equivalente en JavaScript, peror no está tan mal para un lenguaje implementado en menos de 150 ((líneas de código)).
{{id egg_fun}}
{{index función, "lenguaje Egg"}}
Un lenguaje de programación sin funciones es un lenguaje de programación pobre, en efecto.
Afortunadamente, no es difícil agregar un constructo fun
,
que trate su último argumento como el cuerpo de la función y use
todos los argumentos antes de ese como los parámetros de la función.
specialForms.fun = (args, scope) => {
if (!args.length) {
throw new SyntaxError("Functions need a body");
}
let body = args[args.length - 1];
let params = args.slice(0, args.length - 1).map(expr => {
if (expr.type != "word") {
throw new SyntaxError("Parameter names must be words");
}
return expr.name;
});
return function() {
if (arguments.length != params.length) {
throw new TypeError("Wrong number of arguments");
}
let localScope = Object.create(scope);
for (let i = 0; i < arguments.length; i++) {
localScope[params[i]] = arguments[i];
}
return evaluate(body, localScope);
};
};
{{index "ámbito local"}}
Las funciones en Egg tienen su propio ámbito local. La función producida
por la forma fun
crea este entorno local y agrega las asociaciones de
argumentos a este. Entonces evalúa el cuerpo de la función en este ámbito
y regresa el resultado.
run(`
do(define(plusOne, fun(a, +(a, 1))),
imprimir(plusOne(10)))
`);
// → 11
run(`
do(define(pow, fun(base, exp,
si(==(exp, 0),
1,
*(base, pow(base, -(exp, 1)))))),
imprimir(pow(2, 10)))
`);
// → 1024
{{index interpretación, compilación}}
Lo que hemos construido es un intérprete. Durante la evaluación, actúa directamente en la representación del programa producido por el parseador.
{{index eficiencia, rendimiento}}
La Compilación es el proceso de añadir otro paso entre la lectura del programa y su ejecución, que transforma el programa en algo que pueda ser evaluado más eficientemente, haciendo la mayor cantidad de trabajo posible por adelantado. Por ejemplo, en lenguajes bien diseñados es obvio, para cada uso de una ((asignación)), a qué asignación te estás refiriendo, sin correr el programa. Esto puede ser usado para evitar buscar la asociación por nombbre cada vez que se usa, y, en vez de eso, traer directamente el valor de una locación predeterminada de ((memoria)).
Tradicionalmente, la ((compilación)) implica convertir el programa en ((código máquina)), el formato crudo que el procesador de la computadora puede ejecutar. Pero puedes pensar en compilación como cualquier proceso que convierta un programa en una representación diferente.
{{index simplicidad, "función constructor", transpilación}}
Sería posible escribir una estrategia de ((evaluación)) alternativa
para Egg, una que primero convierrta el programa a uno de JavaScript,
usa Function
para invocar el compilador de JavaSctipt, y después correr
el resultado. Cuando se hace correctamente, esto haría que Egg
corrra muy rápiddo mientras todavía sigue siendo simple implementarlo.
Si estás interesado en este tema y dispuesto a gastar algo de tiempo, te animo a que trates de implementar el compilador como ejercicio.
{{index "lenguaje Egg"}}
Cuando definimos id
y while
, probablemente te diste cuenta de que son
más o menos envoltorios triviales alrededor de los if
y while
de
JavaScript. Similarmente, los valores en Egg son solo valores de
JavaScript comunes.
Si comparas la implementación de Egg, construida soobre JavaScript, con la cantidad de trabajo y complejidad requerida para construir un lenguaje de programación directamente en la funcionalidad pura y cruda provista por una máquina, la diferencia es grande. Sin embargo, con suerte este ejemplo te dio un vistazo de la forma en que un ((lenguaje de programación)) funciona.
And when it comes to getting something done, cheating is more effective than doing everything yourself. Though the toy language in this chapter doesn't do anything that couldn't be done better in JavaScript, there are situations where writing small languages helps get real work done.
Such a language does not have to resemble a typical programming language. If JavaScript didn't come equipped with regular expressions, for example, you could write your own parser and evaluator for regular expressions.
{{index "artificial intelligence"}}
Or imagine you are building a giant robotic ((dinosaur)) and need to program its ((behavior)). JavaScript might not be the most effective way to do this. You might instead opt for a language that looks like this:
behavior walk
perform when
destination ahead
actions
move left-foot
move right-foot
behavior attack
perform when
Godzilla in-view
actions
fire laser-eyes
launch arm-rockets
{{index expressivity}}
This is what is usually called a ((domain-specific language)), a language tailored to express a narrow domain of knowledge. Such a language can be more expressive than a general-purpose language because it is designed to describe exactly the things that need to be described in its domain, and nothing else.
{{index "Egg language", "arrays in egg (exercise)"}}
Add support for ((array))s to Egg by adding the following three
functions to the top scope: array(...values)
to construct an array
containing the argument values, length(array)
to get an array's
length, and element(array, n)
to fetch the n^th^ element from an
array.
{{if interactive
// Modify these definitions...
topScope.array = "...";
topScope.length = "...";
topScope.element = "...";
run(`
do(define(sum, fun(array,
do(define(i, 0),
define(sum, 0),
while(<(i, length(array)),
do(define(sum, +(sum, element(array, i))),
define(i, +(i, 1)))),
sum))),
print(sum(array(1, 2, 3))))
`);
// → 6
if}}
{{hint
{{index "arrays in egg (exercise)"}}
The easiest way to do this is to represent Egg arrays with JavaScript arrays.
{{index "slice method"}}
The values added to the top scope must be functions. By using a rest
argument (with triple-dot notation), the definition of array
can be
very simple.
hint}}
{{index closure, [function, scope], "closure in egg (exercise)"}}
The way we have defined fun
allows functions in Egg to reference
the surrounding scope, allowing the function's body to use local
values that were visible at the time the function was defined, just
like JavaScript functions do.
The following program illustrates this: function f
returns a
function that adds its argument to f
's argument, meaning that it
needs access to the local ((scope)) inside f
to be able to use
binding a
.
run(`
do(define(f, fun(a, fun(b, +(a, b)))),
print(f(4)(5)))
`);
// → 9
Go back to the definition of the fun
form and explain which
mechanism causes this to work.
{{hint
{{index closure, "closure in egg (exercise)"}}
Again, we are riding along on a JavaScript mechanism to get the
equivalent feature in Egg. Special forms are passed the local scope in
which they are evaluated so that they can evaluate their subforms in
that scope. The function returned by fun
has access to the scope
argument given to its enclosing function and uses that to create the
function's local ((scope)) when it is called.
{{index compilation}}
This means that the ((prototype)) of the local scope will be the scope in which the function was created, which makes it possible to access bindings in that scope from the function. This is all there is to implementing closure (though to compile it in a way that is actually efficient, you'd need to do some more work).
hint}}
{{index "hash character", "Egg language", "comments in egg (exercise)"}}
It would be nice if we could write ((comment))s in Egg. For example,
whenever we find a hash sign (#
), we could treat the rest of the
line as a comment and ignore it, similar to //
in JavaScript.
{{index "skipSpace function"}}
We do not have to make any big changes to the parser to support this.
We can simply change skipSpace
to skip comments as if they are
((whitespace)) so that all the points where skipSpace
is called will
now also skip comments. Make this change.
{{if interactive
// This is the old skipSpace. Modify it...
function skipSpace(string) {
let first = string.search(/\S/);
if (first == -1) return "";
return string.slice(first);
}
console.log(parse("# hello\nx"));
// → {type: "word", name: "x"}
console.log(parse("a # one\n # two\n()"));
// → {type: "apply",
// operator: {type: "word", name: "a"},
// args: []}
if}}
{{hint
{{index comment, "comments in egg (exercise)"}}
Make sure your solution handles multiple comments in a row, with potentially ((whitespace)) between or after them.
A ((regular expression)) is probably the easiest way to solve this.
Write something that matches "whitespace or a comment, zero or more
times". Use the exec
or match
method and look at the length of the
first element in the returned array (the whole match) to find out how
many characters to slice off.
hint}}
{{index [binding, definition], assignment, "fixing scope (exercise)"}}
Currently, the only way to assign a ((binding)) a value is definir
.
This construct acts as a way both to define new bindings and to give
existing ones a new value.
{{index "local binding"}}
This ((ambiguity)) causes a problem. When you try to give a nonlocal binding a new value, you will end up defining a local one with the same name instead. Some languages work like this by design, but I've always found it an awkward way to handle ((scope)).
{{index "ReferenceError type"}}
Add a special form set
, similar to definir
, which gives a binding a
new value, updating the binding in an outer scope if it doesn't
already exist in the inner scope. If the binding is not defined at
all, throw a ReferenceError
(another standard error type).
{{index "hasOwnProperty method", prototype, "getPrototypeOf function"}}
The technique of representing scopes as simple objects, which has made
things convenient so far, will get in your way a little at this point.
You might want to use the Object.getPrototypeOf
function, which
returns the prototype of an object. Also remember that scopes do not
derive from Object.prototype
, so if you want to call
hasOwnProperty
on them, you have to use this clumsy expression:
Object.prototype.hasOwnProperty.call(scope, name);
{{if interactive
specialForms.set = (args, scope) => {
// Your code here.
};
run(`
hacer(definir(x, 4),
definir(setx, fun(val, set(x, val))),
setx(50),
imprimir(x))
`);
// → 50
run(`set(quux, true)`);
// → Some kind of ReferenceError
if}}
{{hint
{{index [binding, definition], assignment, "getPrototypeOf function", "hasOwnProperty method", "fixing scope (exercise)"}}
You will have to loop through one ((scope)) at a time, using
Object.getPrototypeOf
to go the next outer scope. For each scope,
use hasOwnProperty
to find out whether the binding, indicated by the
name
property of the first argument to set
, exists in that scope.
If it does, set it to the result of evaluating the second argument to
set
and then return that value.
{{index "global scope", "run-time error"}}
If the outermost scope is reached (Object.getPrototypeOf
returns
null) and we haven't found the binding yet, it doesn't exist, and an
error should be thrown.
hint}}