How do compilers implement symbolic (rather than textual) insertion? From a programming language designer's point of view, how is "symbolic"
insertion implemented as opposed to "textual" insertion using pre-processor
macros?

I understand the basics of how "textual" insertion works in C and C++. That is,
the pre-processor might transform:

#include "person.hpp"

class employee 
{
    person p;
};


into:

#ifndef PERSON_HPP
#define PERSON_HPP

class person
{
public:
    uint32_t height_;
};

#endif // PERSON_HPP

class employee
{
    person p;
};


This is required because the size of the employee class can only be determined
if the compiler knows the size of the person class. One disadvantage of this is
slow compilation speed. A program consisting of a single source code file
main.cpp might have thousands of lines inserted into it by the pre-processor
before compilation begins. This can be avoided in some cases by forward
declaring the class and using pointers. The employee class cannot use the
person class until it is defined, (which is why the implementation file will
still include "person.h") but it does remove header file dependencies and
increases compilation speed.

class person;

class employee
{
    person* p;
};


For a language like Java, on the other hand, no textual insertion is required.
Instead, "symbolic" insertion is performed. So the file below would not be
"fleshed out" with the definitions of the dependent software modules.

import com.example.library.person;

class employee {
    private person p;
};


I can't wrap my head around how the compiler accomplishes this. I imagine that
because Java is garbage collected, it doesn't need to know the size before it
is declared, in much the same way as classes can be forward declared in C++
without including the entire file. But how does the compiler know that
p-&gt;some_function_that_does_not_exist(23) is valid?

This question was originally asked on stack overflow.
The Java compiler reads the files you listed with the import statements to see the definitions. For historical reasons C compiler doesn't do this and prefers to get everything in one file, hence the preprocessor.

In the end, there is no magic. If you want to make sure at compiletime that the functions that you're calling exist, then the compiler has to see the definitions. Dynamic languages like Python don't do this check, so they have to see less of the code.