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
13 changes: 12 additions & 1 deletion .github/workflows/html.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
name: Docs & Coverage
name: Deploy

on:
push:
branches:
- master
paths:
- 'src/**/*.cpp'
- 'include/**/*.hpp'
- 'tests/**/*.cpp'
- 'readme.md'

workflow_dispatch:

Expand Down Expand Up @@ -48,6 +53,12 @@ jobs:
token: ${{ secrets.CODECOV_TOKEN }}
verbose: true

- name: Upload coverage to Codacy
uses: codacy/codacy-coverage-reporter-action@a38818475bb21847788496e9f0fddaa4e84955ba
with:
project-token: ${{ secrets.CODACY_PROJECT_TOKEN }}
coverage-reports: build/coverage.info

- name: Clean build
run: rm -rf build/

Expand Down
4 changes: 4 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ on:
pull_request:
branches:
- master
paths:
- 'src/**/*.cpp'
- 'include/**/*.hpp'
- 'tests/**/*.cpp'

jobs:
build-and-test:
Expand Down
46 changes: 40 additions & 6 deletions include/ast.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,30 @@ enum class group_kind { file, body, list, paren, command, item, key, halt };

[[nodiscard]] const char* group_kind_name(group_kind k) noexcept;

/**
* @brief Collection of AST nodes with a configurable size limit.
*
* Large sub-groups may be replaced with placeholder nodes to keep
* @c fixed_size within @c limit.
*/
struct group_node : ast_node {
size_t limit;
size_t limit; ///< Maximum allowed node weight
group_kind kind { group_kind::halt };
std::vector<ast_node_ptr> nodes;
std::priority_queue<std::pair<size_t, size_t>>
weights; /// node_size -> node_index

/// queue of heavy child nodes: <node_size, node_index>
std::priority_queue<std::pair<size_t, size_t>> weights;

/**
* @brief Append a child node while respecting the size limit.
*
* Nodes contribute their @c fixed_size and @c full_size to the parent. If
* the accumulated @c fixed_size exceeds @c limit, larger child groups are
* replaced with ::placeholder_node instances so the tree can be lazily
* expanded later.
*
* @param node Node to append.
* @param src Reader used to reconstruct squeezed subtrees on demand.
*/
void append(ast_node_ptr node, const reader& src);
[[nodiscard]] bool empty() const noexcept override;
[[nodiscard]] size_t size() const noexcept;
Expand All @@ -81,7 +98,17 @@ struct group_node : ast_node {
void dump(
std::ostream& os, const std::string& prefix, bool is_last, bool full
) const override;
const position& get_start() const override;
[[nodiscard]] const position& get_start() const override;
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The [[nodiscard]] attribute is inconsistent with the base class declaration. Either add it to the base class or remove it here for consistency.

Copilot uses AI. Check for mistakes.
/**
* @brief Replace a child group with a placeholder.
*
* The placeholder stores enough information to re-read the original subtree
* from @p src later. This is used when a group's @c fixed_size would exceed
* the configured limit and thus needs to be collapsed.
*
* @param index Index of the child to replace.
* @param src Reader used to recreate the subtree if needed.
*/
void squeeze(size_t index, const reader& src);
void pop_back();
};
Expand All @@ -95,7 +122,14 @@ struct wrapped_node : group_node {

using wrapped_ptr = std::shared_ptr<wrapped_node>;

struct placeholder_node : wrapped_node {
/**
* @brief Node standing in place of a squeezed sub-tree.
*
* When a group exceeds the configured size limit it can be replaced by a
* placeholder node. The original reader is stored so the subtree can be
* reconstructed on demand.
*/
struct placeholder_node final : wrapped_node {
reader* src { nullptr };
void dump(
std::ostream& os, const std::string& prefix, bool is_last, bool full
Expand Down
27 changes: 25 additions & 2 deletions include/expression.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,17 +31,40 @@

class expression {
public:
/**
* @brief Element of the input stream for the expression parser.
* @details When @c is_op is set the item represents an operator token;
* otherwise it stores a pointer to an AST node.
*/
struct item {
bool is_op { false };
token tok;
ast_node_ptr node;
};

/**
* @brief Split a raw node list into tokens and operands.
*
* Consecutive operator tokens are combined into multi-character operators
* such as <tt>+=</tt> or <tt>==</tt>.
*/
static std::vector<item> make_items(const std::vector<ast_node_ptr>& nodes);

/**
* @brief Parse a binary/ternary expression from a token list.
*
* The function implements a Pratt style parser. @p min_prec
* specifies the minimal operator precedence accepted for the
* current recursion level.
*
* @param items Token/operand stream produced by make_items().
* @param idx Current position within @p items, updated on return.
* @param min_prec Minimal precedence level to parse.
*/
static ast_node_ptr
parse_expression(std::vector<item>& items, size_t& idx, int min_prec);

/**
* @brief Parse a prefix expression and any trailing postfix operators.
*/
static ast_node_ptr parse_prefix(std::vector<item>& items, size_t& idx);

private:
Expand Down
57 changes: 48 additions & 9 deletions include/grouper.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,19 @@

#include "ast.hpp"

/**
* @brief Parses tokens into hierarchical groups and expressions.
*
* The grouper is responsible for constructing the AST from a token stream.
* It handles bracket matching, command separation and expression parsing.
*/
class grouper {
public:
explicit grouper(reader& r, size_t limit = 64);

/**
* @brief Parse a sequence starting at the current reader position.
* @param kind Expected top-level group kind.
*/
group_ptr parse(group_kind kind = group_kind::file);

private:
Expand All @@ -43,7 +52,13 @@ class grouper {
void peek();

[[nodiscard]] group_ptr identify_subgroup(const group_ptr& group) const;

/**
* @brief Attach @p inode to the last statement if it is a secondary
* keyword.
*
* Handles constructs like <tt>else</tt> or <tt>catch</tt> by merging them
* with the previous command group.
*/
[[nodiscard]] bool
handle_chain(const group_ptr& result, const group_ptr& inode) const;

Expand All @@ -55,23 +70,47 @@ class grouper {
void identify_body(const group_ptr& group) const;

void identify(const group_ptr& group, const group_ptr& result) const;

/**
* @brief Transform token groups representing arithmetic into AST nodes.
*
* Runs the expression parser over certain group kinds. If the entire group
* forms a valid expression, its children are replaced with the resulting
* expression subtree.
*/
void parse_arithmetic(const group_ptr& group) const;

/**
* @brief Close the current command when a separator is encountered.
*/
bool
append_command(group_ptr& group, group_ptr& top, group_kind kind) const;

/**
* @brief Begin parsing of a bracketed sub-group.
*
* Pushes a new wrapped_node onto @p top when an opening bracket is
* encountered.
*/
void append_wrapped(const group_ptr& top);

/**
* @brief Finalize a wrapped sub-group when a closing bracket is seen.
*/
void close_wrapped(const group_ptr& group, group_ptr& top, group_kind kind);

/**
* @brief Parse a sequence of tokens into the supplied group.
*
* This is the core loop that recognises brackets and separators and
* builds the initial hierarchical structure.
*/
void parse_group(group_kind kind, group_ptr& group);

/**
* @brief Safely append a node to its parent group.
*/
void append(
const group_ptr& parent, const ast_node_ptr& node,
const std::source_location& location = std::source_location::current()
) const;

/**
* @brief Create a formatted runtime error describing a parse failure.
*/
[[nodiscard]] std::runtime_error make_error(
const std::string& message, const group_ptr& context = {},
const std::source_location& location = std::source_location::current()
Expand Down
58 changes: 47 additions & 11 deletions include/reader.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@
#include <fstream>
#include <source_location>

/**
* @brief Byte and line location within the input stream.
*/
struct position {
std::streamoff offset;
int line;
int column;
std::streamoff offset; ///< absolute offset from the beginning of the file
int line; ///< zero based line number
int column; ///< zero based column number
};

enum class token_kind {
Expand All @@ -49,14 +52,14 @@ enum class token_kind {
special_character
};

struct token {
struct token final {
token_kind kind;
position pos;
std::string word;

virtual ~token();
~token();
Comment on lines +55 to +60
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding final to the token struct may be a breaking change if client code previously inherited from token. The destructor was also changed from virtual to non-virtual, which could cause issues if there are existing derived classes.

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the virtual keyword from the destructor while adding final to the class is correct, but this is a breaking change if existing code inherits from token.

Copilot uses AI. Check for mistakes.

virtual void dump(
void dump(
Copy link

Copilot AI Jul 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the virtual keyword from the dump method is a breaking change that could affect polymorphic behavior if client code relies on virtual dispatch.

Copilot uses AI. Check for mistakes.
std::ostream& os, const std::string& prefix, bool is_last
) const noexcept;

Expand All @@ -65,6 +68,13 @@ struct token {

using token_ptr = std::shared_ptr<token>;

/**
* @brief Lightweight tokenizer for QuasiLang source code.
*
* The reader reads from either a file or a memory buffer and produces
* tokens on demand via next_token(). Position information is tracked so
* callers can report meaningful diagnostics.
*/
class reader {
public:
explicit reader(
Expand All @@ -74,11 +84,19 @@ class reader {
explicit reader(std::string& data) noexcept;

~reader();

/**
* @brief Read the next token from the input stream.
*
* @param out Token object to be filled with the parsed data.
*/
void next_token(token& out);

void jump_to_position(position pos);

/**
* @brief Throw an exception with the current position information.
*
* Used by parsers to abort processing while preserving diagnostics.
*/
void interrupt();

position get_position() const;
Expand Down Expand Up @@ -108,15 +126,33 @@ class reader {
void read_whitespace(std::string& into);

void read_keyword(std::string& into);

/**
* @brief Read a quoted string literal with escape handling.
*
* Supports common escape sequences and Unicode escapes of the
* form <tt>\uXXXX</tt>. The resulting decoded text is stored in
* @p into without the surrounding quotes.
* @throw std::runtime_error on malformed input.
*/
void read_string(std::string& into);

void read_comment(std::string& into);

/**
* @brief Parse an integer or floating point literal.
*
* Digits are consumed according to the QuasiLang grammar. If a
* fractional part or exponent is present the returned kind is
* token_kind::floating.
*/
token_kind read_number(std::string& into);

void init_token(token& t) const noexcept;

/**
* @brief Helper to create formatted runtime errors.
*
* In debug builds the message includes context information such
* as the current position and originating source location.
*/
[[nodiscard]] std::runtime_error make_error(
const std::string& message,
const std::source_location& location = std::source_location::current()
Expand Down
Loading
Loading