Trabajo Práctico Especial — Teoría de Lenguajes y Autómatas
Universidad Tecnológica Nacional (UTN) — Facultad Regional Buenos Aires
This repository contains a complete compiler and runtime system for a visual-novel domain-specific language (DSL). The project is built with Flex (lexical analysis), Bison (syntactic analysis), CMake (build), and Raylib (interactive player). It demonstrates the full compiler pipeline: lexing → parsing → semantic analysis → code generation (JSON) → runtime execution.
- Quick Start
- DSL Language Reference
- Project Architecture
- Build & Run
- Testing
- JSON Output Schema
- Docker Development Environment
- CI / CD
- Troubleshooting
Three commands to go from source to a playable visual novel:
# 1. Build the compiler and the Raylib player
src/main/bash/build.sh
# 2. Compile a story file to .build/story.json
src/main/bash/run.sh examples/demo.story
# 3. Play the compiled story
src/main/bash/play.shThe examples/demo.story file exercises every language construct (actors, assets, scenes, dialogue, choices, conditions, arithmetic, goto, and multimedia commands).
All declarations are global and must appear before scene blocks.
character "Display Name" as id color #RRGGBB;Example:
character "Aria" as aria color #FF5733;asset id = "path/to/file";Asset paths are resolved relative to .build/story.json (i.e., the .build/ directory). To reference files at the repository root, prefix with ../:
asset bg = "../src/test/assets/img/background.png";
asset bgm = "../src/test/assets/audio/music.wav";set id = expression;
set id += expression;
set id -= expression;Example:
set score = 0;
set score += 10;
set score -= 5;Scenes are the primary execution scope. The first scene declared is the entry point.
scene "SceneName" {
// statements...
}All executable statements live inside a scene block.
actorId "Text to display.";Example:
aria "Welcome to the story.";show background assetId;
show sprite assetId;
hide background assetId;
hide sprite assetId;Example:
show background bg_forest;
show sprite char_hero;
hide sprite char_hero;play music assetId;
play sound assetId;
stop music assetId;
stop sound assetId;Example:
play music bgm_theme;
play sound sfx_click;
stop sound sfx_click;
stop music bgm_theme;choice {
"Option text" {
// statements for this branch
}
"Another option" {
// statements for this branch
}
}Example:
choice {
"Take the safe path" {
aria "A wise decision.";
goto "Safe_End";
}
"Enter the cave" {
aria "Brave, but risky.";
goto "Dark_End";
}
}if (expression operator expression) {
// then block
} else {
// else block (optional)
}Supported comparison operators: <, <=, >, >=, ==, !=.
Example:
if (score > 5) {
aria "High score!";
} else {
aria "Keep trying.";
}Same syntax as global set, but inside a scene block:
set lives -= 1;
set total = (points * 2) + bonus;Unconditional jump to another scene:
goto "TargetScene";The target scene must exist (verified by the semantic analyzer).
Terminates execution:
end;Expressions are integer-only and support:
- Integer literals:
42,0,-3 - Identifiers (variables):
score,bonus - Parentheses for grouping:
(a + b) * 2 - Arithmetic operators (precedence:
* /before+ -):+addition-subtraction*multiplication/division
Example:
set result = (score + bonus) * 2 - 5;// Single-line comment
/*
* Multi-line comment
*/load "other_file.story";The scanner pushes the loaded file onto the input stack. Its declarations and scenes are merged inline.
| Phase | Tool / Module | Responsibility |
|---|---|---|
| Lexical Analysis | Flex (FlexPatterns.l) |
Tokenizes keywords, identifiers, strings, operators, colors, comments. Supports load "file" for cross-file inclusion. |
| Syntactic Analysis | Bison (BisonGrammar.y) |
Parses tokens into an AST. Uses push-parser, pure interface, and %destructor rules for memory safety. |
| Semantic Analysis | SemanticAnalyzer.c |
Validates: undeclared actors/assets/scenes, duplicate declarations, type mismatches, invalid goto targets. |
| Code Generation | Generator.c |
Walks the AST and emits .build/story.json. |
| Module | File | Responsibility |
|---|---|---|
| Entry Point | player/PlayerEntryPoint.c |
Raylib window/audio init, main render loop, input handling, critical teardown order. |
| Story Loader | player/runtime/StoryLoader.c |
Parses story.json into an in-memory runtime model (Story, Scene, Statement, etc.). |
| Expression Evaluator | player/runtime/ExpressionEvaluator.c |
Evaluates integer expressions and conditions against the runtime variable table. |
| Engine | player/runtime/Engine.c |
Drives the story via a context-stack state machine. Handles dialogue, choice, if/else, goto, set, and all media commands. |
| JSON Parser | player/vendor/cJSON.c |
Vendored MIT-licensed JSON parser (no external network dependency). |
src/main/c/
EntryPoint.c # Compiler entry point
frontend/
lexical-analysis/ # Flex scanner & actions
syntactic-analysis/ # Bison grammar & AST builders
semantic-analysis/ # Semantic validator
backend/
code-generation/ # JSON generator
player/
PlayerEntryPoint.c # Raylib player entry point
runtime/
StoryLoader.c # JSON → runtime model
ExpressionEvaluator.c # Expression / condition evaluator
Engine.c # Story state machine
vendor/
cJSON.c / cJSON.h # Vendored JSON parser
src/test/
c/accept/ # Positive integration tests
c/reject/ # Negative integration tests
assets/ # Generated PNG/WAV test media
examples/
demo.story # Full-featured showcase story
- Docker (recommended for reproducible builds)
- Or native:
gcc,g++,bison,flex,cmake,make,git - Linux / WSL / Docker only (Raylib + GCC + AddressSanitizer)
# Full clean build (deletes generated sources, regenerates them, compiles)
src/main/bash/build.sh- Always deletes
FlexScanner.*andBisonParser.*before CMake. - CMake enforces Bison before Flex because Flex is invoked with
--bison-bridge --bison-locations. - Raylib is fetched automatically via
FetchContent(tag5.5) and linked to both binaries.
src/main/bash/run.sh <program-file>This writes .build/story.json (overwritten each run).
src/main/bash/play.sh [.build/story.json]- The script auto-detects headless environments. If
DISPLAYis missing andxvfb-runis available, it wraps the player automatically. - Asset paths inside
story.jsonare resolved relative to.build/.
All tests are integration tests — no C unit-test framework is used.
# Run the full suite
src/main/bash/test.shWhat the suite verifies:
- Accept tests: Positive language samples in
src/test/c/accept/must compile and exit0. - Reject tests: Negative language samples in
src/test/c/reject/must exit non-zero (semantic or syntactic errors). - JSON generation: After compiling
01-linear-scene,.build/story.jsonmust be non-empty. - Player smoke-test: Compiles
06-graphicsand runs the player underxvfb-run(when headless) to verify the binary does not crash.
cat src/test/c/accept/01-linear-scene | .build/Flex-Bison-Compiler src/test/c/accept/01-linear-sceneOn successful compilation, .build/story.json follows this schema:
{
"meta": {
"version": "1.0",
"source": "path/to/input.story"
},
"actors": [
{ "id": "aria", "name": "Aria", "color": "#FF5733" }
],
"assets": [
{ "id": "bg_forest", "path": "assets/img/forest.png" }
],
"declarations": [
{ "kind": "set", "identifier": "score", "op": "=", "expression": { ... } }
],
"scenes": [
{
"name": "Intro",
"statements": [
{ "kind": "show", "target": "background", "resourceId": "bg_forest" },
{ "kind": "dialogue", "actorId": "aria", "text": "Hello!" },
{ "kind": "choice", "options": [
{ "text": "Yes", "statements": [ ... ] },
{ "text": "No", "statements": [ ... ] }
]},
{ "kind": "if", "condition": { ... }, "then": [ ... ], "else": [ ... ] },
{ "kind": "goto", "scene": "NextScene" },
{ "kind": "end" }
]
}
]
}| Kind | Fields |
|---|---|
| integer | {"kind":"integer","value":42} |
| identifier | {"kind":"identifier","name":"score"} |
| binary | {"kind":"binary","op":"+","left":{...},"right":{...}} |
| Kind | Fields |
|---|---|
| comparison | {"kind":"comparison","op":">","left":{...},"right":{...}} |
# Start an ephemeral dev container
docker compose run --rm compiler
# Inside the container, run build/test/play as usual
src/main/bash/build.sh
src/main/bash/test.sh
# Shutdown afterwards (outside the container)
docker compose down- Compose file:
compose.yaml - Dockerfile:
src/main/docker/compiler/Dockerfile(Ubuntu 24.04 + build tools + X11/OpenGL dev headers for Raylib) - Optional
.envfile is supported forENVIRONMENT,LOG_IGNORED_LEXEMES,LOGGING_LEVEL.
GitHub Actions workflow: .github/workflows/pipeline.yaml
Triggers on every push and on PR closed / reopened.
Steps:
- Install dependencies:
bison cmake flex gcc g++ git make xvfb - Install X11 / OpenGL dev packages for Raylib
- Make bash scripts executable
- Run
build.sh - Run
test.sh - Run dedicated Player test step (headless smoke-test)
Run src/main/bash/run.sh <story-file> before play.sh. The compiler generates .build/story.json.
Asset paths in story.json are resolved relative to .build/story.json (i.e., the .build/ directory). If your assets are at the repository root, use ../ prefix:
asset bg = "../src/test/assets/img/background.png";Raylib requires a display at runtime. In headless environments:
play.shauto-detects missingDISPLAYand wraps the player withxvfb-run --auto-servernum.- If running the player binary directly, use:
xvfb-run --auto-servernum ./.build/Flex-Bison-Player .build/story.json - Audio may be unavailable in pure headless containers; the player guards against this and will not crash.
Player teardown order is critical. Engine_destroy must run before CloseAudioDevice() and CloseWindow(), or AddressSanitizer reports leaks. This is already handled correctly in PlayerEntryPoint.c — do not reorder calls.
Harmless Docker/WSL filesystem timing artifact. Safe to ignore.
You must append it to the add_executable list in CMakeLists.txt. New sources are not auto-discovered. The compiler and player have separate add_executable blocks.
See LICENSE.md.