Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ jobs:
- name: Install dependencies
run: |
sudo apt-get update
sudo apt-get install -y g++ make cmake libgtest-dev
sudo apt-get install -y g++ make cmake libgtest-dev lcov gcovr

- name: Build and install Google Test
run: |
Expand All @@ -32,12 +32,12 @@ jobs:

- name: Run tests
working-directory: ./lc3vm
run: make test
run: make coverage

deploy-docs:
needs: build_and_test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push' # Deploy only on push to main
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
permissions:
contents: write
pages: write
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,8 @@
*.app

build/
docs/
docs/

CMakeFiles/
CMakeCache.txt
cmake_install.cmake
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,49 @@ To build and run this project, you will need:
* `make`
* Google Test (`libgtest-dev` on Debian/Ubuntu systems)
* `doxygen` (optional, for generating documentation)
* `graphviz` (optional, for Doxygen diagrams)

## Building the Project

The project uses a Makefile located in the `lc3vm/` directory.
The project can be built using either CMake or Make. Choose your preferred method below.

### Using CMake

1. **Create a build directory and navigate to it**:

```bash
mkdir build
cd build
```

2. **Generate build files**:

```bash
cmake ..
```

3. **Build the project**:

```bash
cmake --build .
```

This will create the following executables in the `build` directory:
* `lc3vm`: The main VM executable
* `test_runner`: The test executable

4. **Run tests** (optional):

```bash
./test_runner
```

5. **Generate documentation** (optional):

```bash
cmake --build . --target docs
```

### Using Make

1. **Navigate to the `lc3vm` directory**:

Expand Down
27 changes: 25 additions & 2 deletions lc3vm/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,16 @@ cmake_minimum_required(VERSION 3.10)

project(lc3vm CXX)

set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)


option(ENABLE_COVERAGE "Enable coverage reporting" OFF)
if(ENABLE_COVERAGE)
add_compile_options(-g -O0 --coverage)
add_link_options(--coverage)
endif()

include_directories(include)


Expand All @@ -26,7 +32,7 @@ add_executable(test_runner
tests/test_initialization.cpp
tests/test_opcode_execution.cpp
tests/test_disassembly.cpp
src/ls3.cpp
src/lc3.cpp
src/memory.cpp
src/terminal_input.cpp
)
Expand All @@ -39,4 +45,21 @@ if (DOXYGEN_FOUND)
doxygen_add_docs(docs Doxyfile)
endif()

# Add coverage target
if(ENABLE_COVERAGE)
find_program(GCOVR_PATH gcovr)
if(GCOVR_PATH)
add_custom_target(coverage
COMMAND ${CMAKE_COMMAND} -E echo "\nCoverage Summary:"
COMMAND ${GCOVR_PATH} --root ${CMAKE_SOURCE_DIR}
--exclude "tests/.*"
--exclude "build/.*"
--print-summary
COMMAND ${CMAKE_COMMAND} -E echo "\nCoverage report generated at ${CMAKE_BINARY_DIR}/coverage/index.html"
WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
COMMENT "Generating code coverage report..."
)
endif()
endif()

install(TARGETS lc3vm DESTINATION bin)
34 changes: 29 additions & 5 deletions lc3vm/Makefile
Original file line number Diff line number Diff line change
@@ -1,19 +1,30 @@
CC = /usr/bin/g++

CFLAGS = -Wall -Wextra -std=c++14 -Iinclude -O2 -g
# Coverage flags
COVERAGE_FLAGS = -g -O0 --coverage
COVERAGE_LDFLAGS = --coverage

# Default CFLAGS without coverage
CFLAGS = -Wall -Wextra -std=c++17 -Iinclude -O2 -g

# Coverage-enabled CFLAGS
COVERAGE_CFLAGS = $(CFLAGS) $(COVERAGE_FLAGS)

BUILD_DIR = build
TEST_DIR = tests
GTEST_FLAGS = -lgtest -lgtest_main -pthread

VM_SRCS = src/ls3.cpp src/memory.cpp src/terminal_input.cpp
VM_SRCS = src/lc3.cpp src/memory.cpp src/terminal_input.cpp
VM_TEST_SRCS = $(VM_SRCS)

TEST_MAIN_OBJ = $(BUILD_DIR)/test_main.o
TEST_FILES = tests/test_initialization.cpp tests/test_opcode_execution.cpp tests/test_disassembly.cpp
TEST_FILES = tests/test_initialization.cpp \
tests/test_opcode_execution.cpp \
tests/test_disassembly.cpp \
tests/test_integration.cpp
TEST_OBJS = $(TEST_FILES:tests/%.cpp=$(BUILD_DIR)/tests/%.o)

.PHONY: all clean test docs
.PHONY: all clean test docs coverage coverage-clean

all: $(BUILD_DIR)/lc3vm

Expand All @@ -34,12 +45,25 @@ $(BUILD_DIR)/tests/%.o: tests/%.cpp

$(BUILD_DIR)/test_runner: $(TEST_MAIN_OBJ) $(TEST_OBJS) $(VM_SRCS)
mkdir -p $(BUILD_DIR)
$(CC) $(CFLAGS) $(TEST_MAIN_OBJ) $(TEST_OBJS) $(VM_SRCS) -o $(BUILD_DIR)/test_runner $(GTEST_FLAGS) $(LDFLAGS_COVERAGE)
$(CC) $(CFLAGS) $(TEST_MAIN_OBJ) $(TEST_OBJS) $(VM_SRCS) -o $(BUILD_DIR)/test_runner $(GTEST_FLAGS)

docs:
@echo "Generating documentation with Doxygen..."
@doxygen Doxyfile

# Coverage targets
coverage: CFLAGS := $(COVERAGE_CFLAGS)
coverage: LDFLAGS := $(COVERAGE_LDFLAGS)
coverage: clean test
@echo "Generating coverage report..."
@mkdir -p $(BUILD_DIR)/coverage
@echo "Coverage Summary:"
@gcovr --root . \
--exclude "tests/.*" \
--exclude "build/.*" \
--print-summary
@echo "Coverage report generated at $(BUILD_DIR)/coverage/index.html"

clean:
@echo "Cleaning build directory..."
rm -rf $(BUILD_DIR)
9 changes: 8 additions & 1 deletion lc3vm/include/lc3.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ struct CodeSegment {
* to load programs, run the VM, step through instructions, and inspect/modify its state.
*/
class LC3State {
public:
Memory memory; // Made public for testing
private:
Memory memory;
/**
* @brief Array of 16-bit registers.
* This array holds the values of the 16 registers in the LC-3 architecture.
Expand Down Expand Up @@ -112,6 +113,12 @@ class LC3State {
*/
void request_halt() { running = false; }

/**
* @brief Checks if the VM is currently running.
* @return true if the VM is running, false if it has halted.
*/
bool is_running() const { return running; }

// Test helper methods
/**
* @brief Retrieves the value of a specified register.
Expand Down
7 changes: 7 additions & 0 deletions lc3vm/include/memory.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ class Memory {
* Stores 65536 16-bit words.
*/
std::uint16_t memory[MEMORY_MAX];

/**
* @brief Flag to indicate if we're in test mode.
* When true, keyboard input is simulated using memory values.
*/
bool test_mode = false;

/**
* @brief Reads a 16-bit word from the specified memory address.
* Handles memory-mapped I/O for keyboard status (MR_KBSR) and
Expand Down
24 changes: 13 additions & 11 deletions lc3vm/include/terminal_input.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,24 @@
*/

/**
* @brief Enables raw input mode for the terminal.
*
* In raw mode:
* - Input is unbuffered (character by character).
* - Input is not echoed to the screen.
* - Special character processing (like Ctrl+C for SIGINT) might be altered,
* so signal handling needs to be robust.
* @throw std::runtime_error if enabling raw mode fails.
* @brief Enables raw mode for terminal input.
* This disables echo, canonical mode, and other terminal features
* to allow for direct character input.
* @throw std::runtime_error if terminal attributes cannot be modified.
*/
void enable_raw_mode();

/**
* @brief Disables raw input mode, restoring original terminal settings.
* This function should be called before the program exits to ensure the
* terminal is left in a usable state.
* @brief Disables raw mode and restores original terminal settings.
* This should be called before the program exits to ensure the terminal
* is left in a usable state.
*/
void disable_raw_mode();

/**
* @brief Checks if raw mode is currently enabled.
* @return true if raw mode is enabled, false otherwise.
*/
bool is_raw_mode_enabled();

#endif // LC3_TERMINAL_INPUT_H
79 changes: 48 additions & 31 deletions lc3vm/src/ls3.cpp → lc3vm/src/lc3.cpp
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**
* @file ls3.cpp
* @file lc3.cpp
* @brief Implements the LC3State class methods for the LC-3 virtual machine.
*
* This file contains the definitions for the LC-3 instruction set, trap handlers,
Expand All @@ -18,6 +18,7 @@
#include <array>
#include "traps.hpp"
#include "flags.hpp"
#include "keyboard.hpp"
#include <unistd.h>
#include <sstream>
#include <iomanip>
Expand Down Expand Up @@ -135,58 +136,74 @@ void LC3State::ins(LC3State& state, std::uint16_t instr) {
switch (instr & 0xFF) {
case TRAP_GETC:
{
char c_in = 0;
if (read(STDIN_FILENO, &c_in, 1) == 1) {
state.reg[R_R0] = static_cast<std::uint16_t>(c_in);
if (state.memory.test_mode) {
state.reg[R_R0] = state.memory.read(Keyboard::MR_KBDR);
} else {
char c_in = 0;
if (read(STDIN_FILENO, &c_in, 1) == 1) {
state.reg[R_R0] = static_cast<std::uint16_t>(c_in);
}
}
}
state.update_flags(R_R0);
break;
case TRAP_OUT:
std::cout.put(static_cast<char>(state.reg[R_R0]));
std::cout.flush();
if (!state.memory.test_mode) {
std::cout.put(static_cast<char>(state.reg[R_R0]));
std::cout.flush();
}
break;
case TRAP_PUTS: {
std::uint16_t current_char_addr = state.reg[R_R0];
std::uint16_t val = state.memory.read(current_char_addr);
while (val != 0) {
std::cout.put(static_cast<char>(val));
current_char_addr++;
val = state.memory.read(current_char_addr);
if (!state.memory.test_mode) {
std::uint16_t current_char_addr = state.reg[R_R0];
std::uint16_t val = state.memory.read(current_char_addr);
while (val != 0) {
std::cout.put(static_cast<char>(val));
current_char_addr++;
val = state.memory.read(current_char_addr);
}
std::cout.flush();
}
std::cout.flush();
break;
}
case TRAP_IN: {
std::cout << "Enter a character: ";
std::cout.flush();
char c_in_trap = 0;
if (read(STDIN_FILENO, &c_in_trap, 1) == 1) {
std::cout.put(c_in_trap);
if (state.memory.test_mode) {
state.reg[R_R0] = state.memory.read(Keyboard::MR_KBDR);
} else {
std::cout << "Enter a character: ";
std::cout.flush();
state.reg[R_R0] = static_cast<std::uint16_t>(c_in_trap);
char c_in_trap = 0;
if (read(STDIN_FILENO, &c_in_trap, 1) == 1) {
std::cout.put(c_in_trap);
std::cout.flush();
state.reg[R_R0] = static_cast<std::uint16_t>(c_in_trap);
}
}
state.update_flags(R_R0);
break;
}
case TRAP_PUTSP: {
std::uint16_t current_addr = state.reg[R_R0];
std::uint16_t word = state.memory.read(current_addr);
while (word != 0) {
char char1 = word & 0xFF;
std::cout.put(char1);
char char2 = (word >> 8) & 0xFF;
if (char2) {
std::cout.put(char2);
if (!state.memory.test_mode) {
std::uint16_t current_addr = state.reg[R_R0];
std::uint16_t word = state.memory.read(current_addr);
while (word != 0) {
char char1 = word & 0xFF;
std::cout.put(char1);
char char2 = (word >> 8) & 0xFF;
if (char2) {
std::cout.put(char2);
}
current_addr++;
word = state.memory.read(current_addr);
}
current_addr++;
word = state.memory.read(current_addr);
std::cout.flush();
}
std::cout.flush();
break;
}
case TRAP_HALT:
std::cout << "HALT" << std::endl;
if (!state.memory.test_mode) {
std::cout << "HALT" << std::endl;
}
state.running = false;
break;
default:
Expand Down
Loading