Manual Técnico — Intérprete GoScript
Proyecto 2 · Organización de Lenguajes y Compiladores 1
Universidad de San Carlos de Guatemala — Facultad de Ingeniería
Escuela de Ingeniería en Ciencias y Sistemas
Primer Semestre 2026
Autor: Mynor Cifuentes · Carné 201318644
Índice
- Resumen
- Arquitectura general
- Backend: API e intérprete
- Análisis léxico y sintáctico (Jison)
- Árbol de Sintaxis Abstracta (AST)
- Sistema de tipos y valores en tiempo de ejecución
- Operadores y reglas de coerción implícita
- Intérprete (recorrido del AST)
- Tabla de símbolos y manejo de ámbitos
- Manejo de errores
- Funciones embebidas (built-ins)
- Generación de reportes
- Frontend: IDE web
- Compilación y despliegue
- Limitaciones conocidas
1. Resumen
GoScript es un lenguaje interpretado, fuertemente tipado, inspirado en Go, diseñado
como ejercicio académico del curso. El proyecto entrega:
- Un intérprete que recibe código fuente
.gsty produce salida estándar,
errores léxicos/sintácticos/semánticos, una tabla de símbolos y un AST. - Un API HTTP (
POST /api/run) que envuelve al intérprete y devuelve los
reportes ya formateados. - Un IDE web con editor Monaco, múltiples pestañas, persistencia local y
visualización del AST con Graphviz.
El código está dividido en dos paquetes Node.js independientes:
| Paquete | Tecnología | Puerto | Carpeta |
|---|---|---|---|
| Backend | Node.js + Express + TypeScript + Jison | 3001 |
backend/ |
| Frontend | React + Vite + TypeScript + Monaco | 5173 |
frontend/ |
2. Arquitectura general
Diagrama de capas
1 | ┌──────────────────────────────────────────────────────────────────┐ |
Carpetas del backend
1 | backend/src/ |
Carpetas del frontend
1 | frontend/src/ |
3. Backend: API e intérprete
3.1. Endpoints HTTP
backend/src/server.ts expone dos rutas:
| Método | Ruta | Descripción |
|---|---|---|
GET |
/api/health |
Sondeo simple. Devuelve { ok, service, version }. |
POST |
/api/run |
Recibe { source: string } y devuelve el resultado de ejecutar el código. |
Respuesta de /api/run (interfaz InterpretResult en runner.ts):
1 | { |
3.2. Flujo de ejecución (runner.ts → interpret())
- Crea un
ErrorReportervacío. - Llama
parseGoScript(source, reporter)que:- Construye el parser Jison.
- Inyecta
parseStringyparseRuneenparser.yy(escapes de strings y runes). - Sustituye los handlers de error léxico/sintáctico para que escriban en el
reportercon línea y columna humanas (1-based).
- Si el parser falla, devuelve un resultado vacío con la lista de errores.
- Si tiene éxito, instancia
Interpretery llamainterp.run(ast). - Genera los tres reportes (
astDot,symbolsHtml,errorsHtml) y devuelve
todo junto. sanitizeSymbolsconvierte los valores no serializables a string para que
la respuesta JSON nunca falle.
4. Análisis léxico y sintáctico (Jison)
El archivo backend/src/grammar/goscript.jison define en un único archivo:
- La sección
%lexcon las reglas léxicas. - La sección de gramática con sus acciones semánticas que construyen el AST.
4.1. Tokens léxicos
| Categoría | Tokens |
|---|---|
| Palabras reservadas | func var if else for switch case default break continue return struct range nil true false int float64 string bool rune |
| Literales | INT_LIT, FLOAT_LIT, STRING_LIT, RUNE_LIT |
| Identificadores | IDENT |
| Operadores | := += -= ++ -- == != <= >= && || = < > + - * / % ! |
| Delimitadores | ( ) { } [ ] , ; : . |
| Especiales | <<EOF>>, LEXICAL_ERROR (cualquier caracter no reconocido) |
4.2. Comentarios
- Línea:
// hasta fin de línea - Bloque:
/* … */(no anidados, soporta multilínea).
4.3. Escapes en strings y runes
lexer-helpers.ts maneja: \", \', \\, \n, \r, \t. Cualquier otro
escape se interpreta como el carácter literal que sigue.
4.4. Precedencia y asociatividad
1 | %left OR |
Estas directivas resuelven la ambigüedad clásica de expr op expr op expr.
4.5. Resolución de ambigüedad: cond_expr vs. expr
Para evitar el conflicto entre el bloque que abre con { y un struct literal
que también empieza con IDENT '{' …, la condición de las sentencias if,for y switch usa la regla cond_expr que excluye el struct literal
de primer nivel:
1 | expr : binary_expr | struct_literal ; |
De este modo if Persona{Nombre:"Ana"} { … } no es ambiguo: la condición sólo
acepta operaciones, comparaciones e identificadores; los struct literals deben
ir precedidos por (...) o como parte de una expresión más interna.
4.6. Conflictos shift-reduce conocidos y resueltos
La gramática tiene varios conflictos shift-reduce que Jison resuelve a favor de
shift (lo deseado en cada caso):
IDENTseguido de{: prefiere construir unstruct_literalantes que reducir
el identificador a expresión simple.IDENTseguido de otroIDENT: prefiere la regla de declaración estilo C
(Tipo nombre = expr) antes que reducir a expresión.RETURNseguido de un token de expresión: prefiereRETURN exprsobreRETURNdesnudo.
Todos estos conflictos están documentados en el archivo goscript.jison.
5. Árbol de Sintaxis Abstracta (AST)
backend/src/ast/nodes.ts define los nodos como uniones discriminadas de
TypeScript: cada nodo tiene un campo kind literal que determina su forma, y
un campo loc: { line, column } con la posición del primer token que lo
originó.
5.1. Categorías de nodos
1 | Program |
5.2. Nodos compuestos relevantes
| Nodo | Campos clave |
|---|---|
FunctionDecl |
name, params, returnType, body |
StructDecl |
name, fields: FieldDecl[] |
VarDecl |
name, declaredType, value, inferred |
IfStmt |
condition, consequent, alternate (puede encadenarse en else if) |
SwitchStmt |
expression, cases: CaseClause[], defaultBody |
ForStmt |
init, condition, post, body (cubre las 3 variantes con ;) |
ForRangeStmt |
indexName, valueName, iterable, body |
SliceLiteral |
elementType (puede ser null si el tipo se hereda del padre), elements |
StructLiteral |
typeName, fields: StructFieldInit[] |
AnonymousLiteral |
fields: AnonymousField[] (cada uno con name? y value) |
5.3. Composite literal anónimo
Para soportar la sintaxis estilo C Chip c = { serie: "X", … }, además de los
slices abreviados [][]int{ {1,2}, {3,4} }, el AST incluye dos nodos
flexibles que se materializan en tiempo de evaluación según el tipo destino:
AnonymousLiteralse usa como RHS de declaraciones tipoT x = { … }.SliceLiteralconelementType: nullse usa como elemento de un slice
multidimensional cuyo tipo se hereda del slice padre.
Ambos casos los resuelve el intérprete en evalAnonymousLiteral yevalSliceLiteral propagando el tipo desde el contexto.
6. Sistema de tipos y valores en tiempo de ejecución
6.1. TypeRef (backend/src/ast/types.ts)
Descripción estructural unificada de los tipos del lenguaje:
1 | type TypeRef = |
Funciones utilitarias:
typeToString(t)→ representación legible ([]int,Persona,float64…).sameType(a, b)→ igualdad estructural.isNumericType(t)→ conveniencia para coerciones.
6.2. GValue (backend/src/runtime/values.ts)
Cada valor en tiempo de ejecución es una pareja { type, value }:
1 | interface GValue { type: TypeRef; value: any } |
Constructores: vInt, vFloat, vString, vBool, vRune, vNil,vSlice(elemType, items), vStruct(name, fields).
Funciones clave:
defaultFor(type)— valor por defecto al declararvar x Tsin valor:int → 0,float64 → 0.0,string → "",bool → false,rune → 0,slice → nil(value =null),struct → nil(value =null).
formatValue(v)— formateo parafmt.Println:float64con valor entero se imprime como3.0(no3).slicese imprime como[a b c](separador espacio).structse imprime comoPersona{nombre: Ana, edad: 25}.runese imprime como su carácter.
7. Operadores y reglas de coerción implícita
backend/src/runtime/operators.ts implementa las tablas exigidas por el
enunciado. Cada función toma dos GValue y devuelve uno; si la combinación
de tipos no es válida, lanza OpError que el intérprete convierte en error
semántico.
7.1. Suma +
| izq \ der | int | float64 | string | bool | rune |
|---|---|---|---|---|---|
| int | int | float64 | string | int | int |
| float64 | float64 | float64 | string | float64 | float64 |
| string | string | string | string | string | string |
| bool | int | float64 | string | bool (OR) | int |
| rune | int | float64 | string | int | int |
Reglas resumidas:
- Si alguno es
string, el otro se convierte y se concatena. - Si alguno es
float64, el resultado esfloat64. bool + boolaplicaORlógico:true + false = true.runese promueve aintpara sumar con enteros.
7.2. Resta - y multiplicación *
string - Xno está permitido; produce error.string * int(en cualquier orden) repite la cadena ("ab" * 3 = "ababab").bool * boolaplicaANDlógico.- Las demás combinaciones siguen la regla “el más amplio gana”
(float64 > int > rune > bool).
7.3. División / y módulo %
/sólo entre numéricos (int,float64).int / intdaint(división truncada hacia cero).- Cualquier operando
float64producefloat64. %sólo entreint. Cualquier otra combinación → error.- División o módulo entre cero → error de ejecución.
7.4. Negación unaria -
Aplica a int, float64, rune, bool (este último convertido a 0/1).
7.5. Comparación ==, !=, <, <=, >, >=
- Numéricos (
int,float64,rune) se comparan promoviendo al más amplio. string == stringybool == boolpor valor.niles igual anil(incluyendo slices y structsnil).<,<=,>,>=aceptan numéricos y strings; no aceptanboolninil.
7.6. Lógicos &&, ||, !
Operan únicamente sobre bool; cualquier otro tipo lanza error semántico.
Implementan cortocircuito:
false && Xno evalúaX.true || Xno evalúaX.
7.7. Coerción al asignar (coerceForAssign)
Interpreter.ts aplica una promoción mínima al asignar:
intorune→float64: se convierte automáticamente.nil→sliceostruct: se permite (slice/struct nil).- Mismo tipo: pasa sin cambios.
- En cualquier otro caso desigual: error semántico.
8. Intérprete (recorrido del AST)
backend/src/interpreter/Interpreter.ts implementa un evaluador clásico de
recorrido en árbol con dos pasadas:
8.1. Pasada 1 — Registro global
Recorre program.declarations y registra:
- Funciones (
registerFunction): se valida que no choquen con otro símbolo
global. Se anotan en la tabla de símbolos comoFuncion. - Structs (
registerStruct): igual, anotados comoStruct. - Variables globales (
execVarDeclcon env =Global).
8.2. Pasada 2 — Ejecución de main
- Si no existe
main, registra error semántico y retorna. - Crea un
Environmenthijo del global llamadomainy llamacallFunction.
8.3. Señales de control de flujo
Para implementar break, continue y return sin coste de instrumentar todo
el árbol con guards manuales, el intérprete usa excepciones JavaScript como
señales:
BreakSignal— la captura elforo elswitchenvolvente.ContinueSignal— la captura elforenvolvente.ReturnSignal— la capturacallFunctiony devuelve el valor.
Si una señal escapa al nivel global (por ejemplo un break fuera de un loop),handleError la convierte en error semántico con la ubicación correspondiente.
8.4. Llamadas a función
evalCall decide la naturaleza de la llamada:
- Si el nombre cualificado (
fmt.Println,len, …) corresponde a un
built-in registrado, lo invoca con los argumentos ya evaluados. - Si el callee es un
Identifierque coincide con una función declarada por
el usuario, llamacallFunction(fn, args, loc). - En otro caso, registra error semántico.
callFunction valida la cantidad de argumentos, crea un environment cuyo
padre es siempre el global (no captura el ámbito local del llamador,
imitando la semántica de Go sin closures), y declara los parámetros con la
coerción correspondiente.
8.5. Recursión
La recursión se soporta de forma natural porque las funciones se registran
antes de ejecutar main. El intérprete fue probado contra tribonacci(6),mcdEuclides(48,18), potenciaRecursiva(2,10) y sumaNaturales(10) con
profundidades de hasta varios cientos de llamadas anidadas.
8.6. for-range sobre slices y strings
execForRangeStmt itera sobre el iterable declarando dos variables locales
por iteración:
indexNamecon tipointy el índice secuencial.valueName(opcional) con el valor actual:- Si el iterable es un
[]T, el valor tiene tipoTy es el elemento. - Si el iterable es un
string, el valor tiene tiporuney es el
codepoint del carácter actual; los caracteres se separan por
code points (compatibles con Unicode).
- Si el iterable es un
El identificador _ se acepta en posición de variable porque la gramática
lo trata como cualquier otro IDENT; sin embargo, no se realiza ningún
tratamiento especial: declarar dos veces _ en el mismo ámbito sí causa
error. En la práctica los ejemplos usan _ solo una vez por iteración.
8.7. Acceso y mutación de structs anidados
Los structs se representan internamente como Record<string, GValue> (un
objeto de JavaScript), por lo que el acceso area.equipo.chip.serie resuelve
referencias compartidas: mutar el struct más profundo se refleja en el padre
sin necesidad de copias explícitas.
9. Tabla de símbolos y manejo de ámbitos
9.1. Environment (backend/src/symbols/Environment.ts)
Cada bloque ({ … }), cuerpo de función, for o switch crea unEnvironment hijo. La búsqueda de un símbolo (lookup) sube por la cadena
de padres hasta encontrarlo o llegar al global.
Operaciones soportadas:
declare(sym)— error si el nombre ya existe en el ámbito local
(no impide ocultar uno del padre).lookup(name)— recorrido ascendente.lookupLocal(name)— sólo en el ámbito actual.assign(name, value)— actualización in-place.
9.2. SymbolTable
Acumula todos los símbolos vistos durante la ejecución (incluidos los que
ya salieron de su ámbito) para producir el reporte final. Cada entradaSymbolInfo registra: name, kind, type, value, scopeName, line,column, mutable.
Las clases de símbolos son: Variable, Parametro, Funcion, Struct.
10. Manejo de errores
10.1. ErrorReporter
Singleton por ejecución. Cada error guarda:
1 | { id, kind: 'Léxico'|'Sintáctico'|'Semántico', description, line, column } |
10.2. Origen de cada categoría
- Léxico — el lexer encuentra un carácter fuera del alfabeto (regla
LEXICAL_ERROR). Lo interceptaparser.lexer.parseErrorenparser.ts. - Sintáctico — el parser Jison falla. Lo intercepta
parser.yy.parseError
con la línea/columna y el token encontrado. - Semántico — registrado por el intérprete cuando un tipo no encaja, una
variable no está declarada, una función recibe argumentos incorrectos, hay
división entre cero, etc.
10.3. Resiliencia
El intérprete intenta continuar después de la mayoría de los errores
semánticos: por ejemplo, si una variable no existe, devuelve nil y reporta
el error en lugar de abortar. Esto permite ver varios errores en una sola
ejecución, lo cual mejora la experiencia en el IDE.
11. Funciones embebidas (built-ins)
Definidas en backend/src/builtins/index.ts. Cada built-in es una función(args, ctx) => GValue registrada en un Map global.
| Nombre | Firma | Notas |
|---|---|---|
fmt.Println |
(...any) → void |
Imprime con espacio entre argumentos y \n final. |
fmt.Print |
(...any) → void |
Igual a Println pero sin salto de línea. |
strconv.Atoi |
(string) → int |
Devuelve 0 y reporta error si la conversión falla. |
strconv.ParseFloat |
(string [, int]) → float64 |
El segundo argumento (precision) se acepta pero se ignora. |
reflect.TypeOf |
(any) → string |
Devuelve la representación textual del tipo. |
len |
(string | []T) → int |
nil slice devuelve 0. |
append |
([]T, ...T) → []T |
Crea un nuevo slice; no muta el original. |
slices.Index |
([]T, T) → int |
Devuelve -1 si no se encuentra. Comparación primitiva (===). |
strings.Join |
([]string, string) → string |
El separador debe ser string. |
El contexto BuiltinContext provee:
print(text)yprintln(text)— escriben al buffer de salida.reportError(msg)— registra un error semántico anclado a la línea/columna
de la llamada.
12. Generación de reportes
12.1. AST → DOT (Graphviz) — reports/astDot.ts
Recorre el AST completo asignando un identificador entero a cada nodo y
emitiendo nodos y aristas con etiquetas y colores por categoría:
- Verde para declaraciones globales.
- Azul para sentencias (if, for, switch, return, …).
- Naranja para operaciones binarias/unarias.
- Morado para llamadas.
El frontend renderiza este DOT con @viz-js/viz para producir un SVG
interactivo descargable.
12.2. Tabla de símbolos → HTML — reports/symbolsHtml.ts
Genera un documento HTML autocontenido (estilos inline) con una tabla de
todos los símbolos. Se devuelve en la respuesta JSON y el IDE permite
descargarlo desde el panel Tabla de Símbolos.
12.3. Errores → HTML — reports/errorsHtml.ts
Mismo enfoque que la tabla de símbolos. Cada fila incluye id, tipo, descripción,
línea y columna, con codificación de color por categoría (rojo/amarillo/azul).
13. Frontend: IDE web
13.1. Stack y estructura
- React 19 con hooks (
useState,useEffect,useRef). - Vite 8 como bundler y servidor de desarrollo (puerto
5173). - Monaco Editor vía
@monaco-editor/reactcon temavs-darky resaltado
de Go (suficiente para GoScript). - Viz.js vía
@viz-js/viz(instancia única cargada bajo demanda). - Sin librería de estado global; toda la lógica vive en
App.tsx.
13.2. Modelo de pestaña
1 | interface FileTab { |
Cada pestaña conserva sus propios resultados, así el usuario puede alternar
entre archivos sin perder la última ejecución de cada uno.
13.3. Persistencia (localStorage)
- Clave:
goscript-ide:workspace:v1. - Se guardan únicamente
id,name,code,dirty(no resultados, son
pesados y se recalculan ejecutando). - Debounce de 300 ms sobre cada cambio de tabs/activeId.
- Al cerrar la ventana con tabs sucias, se dispara el aviso
beforeunload.
13.4. Atajos de teclado
| Combinación | Acción |
|---|---|
Ctrl/Cmd + S |
Guardar la pestaña activa (descarga .gst). |
Ctrl/Cmd + N |
Nueva pestaña en blanco. |
Ctrl/Cmd + W |
Cerrar la pestaña activa (con confirmación si está sucia). |
13.5. Llamada al backend
handleRun envía POST {API_BASE}/api/run con { source }. La URL del API
puede sobrescribirse con la variable de entorno VITE_API_BASE al construir
el frontend; por defecto apunta a http://localhost:3001.
13.6. Renderizado del AST
El SVG del AST se calcula bajo demanda al abrir el panel AST, y queda
cacheado por pestaña. Si el DOT cambia (porque se reejecutó el código), se
limpia el SVG y se vuelve a renderizar.
14. Compilación y despliegue
14.1. Backend
1 | cd backend |
Variables de entorno opcionales:
PORT— puerto del API (por defecto3001).
14.2. Frontend
1 | cd frontend |
Variables de entorno (Vite):
VITE_API_BASE— URL del API. Por defectohttp://localhost:3001.
14.3. CLI auxiliar
backend/scripts/test-interpreter.js permite ejecutar archivos .gst desde
la terminal sin levantar el frontend, mostrando salida, errores y tabla de
símbolos. Útil para regresiones rápidas:
1 | node backend/scripts/test-interpreter.js examples/evaluacion_funciones_prueba.gst |
15. Limitaciones conocidas
Las siguientes funcionalidades no están implementadas porque quedan fuera
del alcance del enunciado:
- Closures — las funciones no capturan variables del ámbito del llamador.
Su entorno padre es siempre el global. - Funciones de orden superior — no se pueden pasar funciones como
argumentos ni guardarlas en variables. - Múltiples valores de retorno — cada función devuelve a lo sumo un
GValue(o ninguno). - Maps y canales — no se incluyen las estructuras
mapnichande Go. - Goroutines y
select— el lenguaje es estrictamente secuencial. - Punteros y referencias explícitas — los slices y structs son objetos
compartidos por referencia, pero no hay sintaxis*Tni&x. - Imports — los
paquetes(fmt,strconv, etc.) se reconocen como
prefijos sintácticos en las llamadas; no hay un sistema de módulos real. - Tipos con métodos — los structs no tienen métodos asociados, solo
campos.
Estas limitaciones se documentan también en el Manual de Usuario.



