Undefined reference to library functions

I had to write a small library for a calculator in arduino, and I did, but woe is me I actually made a C++ library, not an arduino one, so I tried to port it to arduino the best I could. After a long fight with the compiler I managed to make it recognize the correctness of the code (it compiled, alongside the sketch, the library files, it even recognized its functions when I invoked one incorrectly) but in the end it looks like the linker refuses to recognize my library.
My project directory structure has an include directory with every source file, and outside this include directory (the root of the sketch) theres my sketch main file. In it I #include my library via #include "include/parser.hpp" (parser.hpp being the header file I wish to use).

Main sketch file:

#include "include/parser.hpp"

void setup(){
	Serial.begin(9600);
}

void loop(){
	String input = Serial.readString(); // Leemos la entrada.
	Parser myParser(input.c_str());
	AST *Tree = myParser.parse();
	
	Serial.println(Tree->eval(Tree->getRoot())); // Imprimimos.

	delete Tree;
}

myParser is a Parser type object which has a parse function that, when invoked, it returns a pointer to an AST object (if it didn't encounter any errors, that is).

parser.hpp:

#include <Arduino.h>
#include <Vector.h>
#include "lexer.hpp"
#include "ast.hpp"
#ifndef PARSER_H
#define PARSER_H
/* La pila que guardará los nodos del árbol tendrá una capacidad inicial de 4
   nodos, al final terminaremos con un solo nodo en la pila (la raíz del árbol)
   y regularmente tendremos al menos 2 nodos. (dos operandos). */
#define INITIAL_STACK_SIZE 5

class ParserException{
private:
  String error;
public:
  ParserException(String error_msg);
  String getError() const; 
};

class Parser{
private:
  Lexer tokenizer;
  char *text;
  Token current_token;
  Vector< Node * >node_stack;
  void nextToken();
  Node *popStack();
  bool parseExpression();
  bool parsePrimeExpression();
  bool parseTerm();
  bool parsePrimeTerm();
  bool parseFactor();
public:
  Parser(char *text);
  AST *parse();
};
#endif /* PARSER_H */

Parser.cpp:

#include <Arduino.h>
#include <Vector.h>
#include "parser.hpp"
#include "lexer.hpp"
#include "ast.hpp"

ParserException::ParserException(String error_msg)
  : error{error_msg}{}

String ParserException::getError() const{
  return error;
}

Parser::Parser(char *text)
    : tokenizer(text){
  Node node_array[INITIAL_STACK_SIZE];
  nextToken();
  node_stack(node_array);
}

AST *Parser::parse(){
  if(!parseExpression())
    throw ParserException("SYNTAX ERROR");

  /* De haber salido todo bien, habrá un solo
     nodo en la pila: la raíz del árbol. */
  AST *syntaxTree = new AST(popStack());

  return syntaxTree;
}

void Parser::nextToken(){
  current_token = tokenizer.nextToken();
}

Node *Parser::popStack(){
  Node *new_node = node_stack.back();
  node_stack.pop_back(); 

  return new_node;
}

bool Parser::parseExpression(){
  if(!parseTerm())
    return false;

  return parsePrimeExpression();
}

bool Parser::parsePrimeExpression(){
  if(current_token.type == sum || current_token.type == substraction)
  {
    Token current_operator = current_token;
    nextToken();
    
    if(!parseTerm())
      return false;

    node_stack.push_back(new Node{popStack(), popStack(), current_operator});
    
    return parsePrimeExpression();
  }
  
  return true;
}

bool Parser::parseTerm(){
  if(!parseFactor())
    return false;

  return parsePrimeTerm();
}

bool Parser::parsePrimeTerm(){
  if(current_token.type == multiplication || current_token.type == division)
  {
    Token current_operator = current_token;
    nextToken();
      
    if(!parseFactor())
      return false;

    node_stack.push_back(new Node{popStack(), popStack(), current_operator});
      
    return parsePrimeTerm();
  }
  
  return true;
}

bool Parser::parseFactor(){
  Token current_operator;

  switch(current_token.type)
  {
  case substraction:
    current_operator = current_token;  // Nodo de negación.
    nextToken();
    
    if(!parseExpression())
      return false;

    node_stack.push_back(new Node{popStack(), NULL, current_operator});

    break;
  case left_parens:
    nextToken();
    
    if(!parseExpression())
      return false;

    if(current_token.type != right_parens)
      return false;
    
    break;
  case number:
    node_stack.push_back(new Node{NULL, NULL, current_token});
    nextToken();
    
    break;
  default:
    return false;
  }

  return true;
}

ast.hpp:

#include <Arduino.h>
#include "lexer.hpp"
#ifndef AST_H
#define AST_H

struct Node {
  Node *left {};
  Node *right {};
  Token value {};
};

class AST{
private:
  Node *root;
  void postOrderDelete(Node *current); // Recorrido del árbol en postorden.
public:
  AST(Node *tree_root);
  ~AST();
  Node *getRoot() const;
  float eval(const Node *current);
};

class ZeroDivisionException{
private:
  String error_msg;
public:
  ZeroDivisionException(String msg);
  String getErrorMsg() const;
};

class invalid_argument{
private:
  String error_msg;
public:
  invalid_argument(String msg);
  String getErrorMsg() const;
};

#endif /* AST_H */

ast.cpp:

#include <stdio.h>
#include <Arduino.h>
#include "ast.hpp"
#include "lexer.hpp"

AST::AST(Node *tree_root)
  : root{tree_root}{}

void AST::postOrderDelete(Node *current) {
  if(current->left)
    postOrderDelete(current->left);

  if(current->right)
    postOrderDelete(current->right);

  if(current->value.type == number)
    delete [] current->value.value; // Borramos también la cadena de carácteres asignada.

  delete current;
}

AST::~AST() {
  postOrderDelete(root);
}

Node *AST::getRoot() const {
  return root;
}

float AST::eval(const Node *current){
  float result;

  // Checamos si tenemos un nodo de negación.
  if(current->left != NULL && current->right == NULL)
    return -1 * eval(current->left);
  
  switch(current->value.type)
  {
  case sum:
    return eval(current->left) + eval(current->right);
  case substraction:
    return eval(current->left) - eval(current->right);
  case multiplication:
    return eval(current->left) * eval(current->right);
  case division:
  {
    float denominator = eval(current->left);

    if(denominator == 0)
      throw ZeroDivisionException("División entre 0.");
    
    return eval(current->right) / denominator;
  }
  default: // Es un número.
    if(sscanf(current->value.value, "%f", &result) == EOF)
      throw invalid_argument("Conversión fallida.");

    return result;
  }
}

ZeroDivisionException::ZeroDivisionException(String msg)
  : error_msg{msg} {}

String ZeroDivisionException::getErrorMsg() const{
  return error_msg;
}

invalid_argument::invalid_argument(String msg)
  : error_msg{msg} {}

String invalid_argument::getErrorMsg() const{
  return error_msg;
}

Compiler error message:

arduino-compile
/tmp/ccYGJyKY.ltrans0.ltrans.o: In function `loop':
/home/sebastian/Documentos/LCC/6to Semestre/Microprocesadores/parser_example/parser_example.ino:9: undefined reference to `Parser::Parser(char*)'
/home/sebastian/Documentos/LCC/6to Semestre/Microprocesadores/parser_example/parser_example.ino:10: undefined reference to `Parser::parse()'
/home/sebastian/Documentos/LCC/6to Semestre/Microprocesadores/parser_example/parser_example.ino:12: undefined reference to `AST::getRoot() const'
/home/sebastian/Documentos/LCC/6to Semestre/Microprocesadores/parser_example/parser_example.ino:12: undefined reference to `AST::eval(Node const*)'
/home/sebastian/Documentos/LCC/6to Semestre/Microprocesadores/parser_example/parser_example.ino:14: undefined reference to `AST::~AST()'

Note: I'm not using the IDE, I'm using arduino-cli for both compilation and uploading.

I hope you are not planning to release this

I am not expert in this, but if you put your library in quotes, it either has to be in the project folder, or the path in quotes has to be absolute all the way to /home.

I think this is the cause of the problem. Recursive compilation of sketch source files is only done under the src subfolder:
https://arduino.github.io/arduino-cli/latest/sketch-specification/#src-subfolder

So your .cpp files under the include subfolder are never compiled.

Yeah, it's on the project folder.

Tried the following directory structure, no luck still:

-- example.ino
|
|-src
|
|-Parser
|
|-src
|
|- parser.h
|-parser.cpp
... etc.

Were they really not getting compiled? In the first tries the compiler seemed to try, pointing to errors and such.

What's wrong with it? Does arduino handle dinamically allocated pointers automatically at the end of the loop?

-- example.ino
|
|-src
|
|-Parser
|
|-src
|
|- parser.h
|-parser.cpp
... etc.

Well it seems it messed up the formatting, the root directory has the main .ino file and a src directory with another directory named Parser inside, and this one has another src directory with all the library source files as per the directory structure example in here.

Tried with the library source files on the root folder, and well it worked. But how can I get the compiler to compile the library while it sits in a separate directory?

Are you still getting the exact same error message as you shared before?

If not, please post the new error messages.

You were getting the declarations from the header file added to your sketch via the #include directives, but you weren't getting the definitions from the source files, thus the undefined reference to ... errors.

This is an unfortunate quirk of the Markdown markup language the forum uses to allow use to do nice formatting of our posts. Markdown strips all leading spaces

The way to prevent that is to wrap the text you don't want formatted in "fenced code blocks", which is three backticks (`) on the line above and below the text:

```
-- example.ino
|
|-src
   |
   |-Parser
   |
   |-src
     |
     |- parser.h
     |-parser.cpp
     ... etc.
```

(I guessed at the original structure of the text, but you get the idea)

Well, you could just install the library the normal way, under the libraries subfolder of the user directory (aka "sketchbook folder" for the IDE users). The location is shown in the directories.user key of the arduino-cli config dump output.

But Arduino CLI does allow you to specify custom library locations via the arduino-cli compile --library flag.

Either way, the library must be compliant with the Arduino Library Specification. There is quite a bit of flexibility in the formats of valid Arduino libraries. Some struggle with it at first, but I think they usually end up finding a way to create a structure both they and the Arduino build system find acceptable.

Thanks, installing it via a .zip file in the IDE made the trick. Sadly my library is everything but arduino compliant, I guess I'll have to rewrite a large portion of the code, still, thanks for the help.

You are welcome. I'm glad it is working now.

I really don't think any rewriting of code should be needed. Making a library Arduino compatible is y just a matter of getting the files in the right folders and adding a simple library.properties metadata file if you chose the "1.5 format". The only code changes that should be necessary are adjusting the #include directives to reflect any changes in relative file locations that might have been needed.

If you would like assistance, you are welcome to put the library in a .zip file and post it in an attachment here on the forum. It is likely the forum members will be able to provide more specific advice after having a look at the exact structure.

The compiler issues some errors about my struct initializations and usage and exception handling. I issued the -fexceptions compiler flag but still, I think I shouldn't use exceptons altogether.

If I come accross any headache inducing problems while making the library more arduino friendly, I'll make sure to ask here. Thanks for the help.