Now I have the complete picture. Here's the explanation:
How #INCLUDE "course.module" Works
The include mechanism has three layers: the C compiler's file resolution, the occbuild Python tool that generates .module files and manages search paths, and the build system (SCons/autoconf) that orchestrates everything.
Layer 1: The C Compiler (occ21)
When the compiler parses #INCLUDE "course.module" in syn2.c:6086 (rfile()), it treats it as a plain source include (LEX_SOURCE mode). It calls open_file() at lex1.c:1234, which calls:
fptr = popen_relative(name, current_fe_data->fe_pathname, NULL, "r", ...);
The second argument, fe_pathname, defaults to the string "ISEARCH" (misc/harness.c:331). This tells popen_relative() (in fnilib/popen.c:130) to read the ISEARCH environment variable, split it into a colon-separated list of directories, and search each one for the file. The compiler itself knows nothing about modules, build systems, or Python scripts -- it just searches directories listed in ISEARCH.
Layer 2: occbuild -- The Module Builder
The real intelligence lives in tools/kroc/occbuild.in, a Python script. It does two critical things:
A) Generates .module files. The make_module() function (occbuild.in:290) creates course.module as a real occam source file on disk. For the course library, the generated file looks roughly like:
-- Include file for "course" module
#IF NOT (DEFINED (COURSE.MODULE))
#DEFINE COURSE.MODULE
#INCLUDE "forall.module"
#INCLUDE "consts.inc"
... toolchain-specific PRAGMA directives ...
#ENDIF
The content is controlled by --need (adds #INCLUDE "X.module" for dependencies), --include (adds custom #INCLUDE lines), and a toolchain-specific callback that adds #PRAGMA directives to link the compiled library.
B) Sets the ISEARCH environment variable. Before invoking occ21, occbuild builds a search path from multiple sources and calls set_isearch() (occbuild.in:260) to export it:
- In-tree module directories -- resolved from the
in-tree-modulesfile (see Layer 3) --searchflags -- passed by the build system (occbuild.in:1071)- Existing
ISEARCH-- any user-set value is appended (occbuild.in:256) - Toolchain defaults -- the installed library path (
occbuild.in:254)
Layer 3: The Build System Wiring
SCons (site_scons/site_tools/occbuild.py): Each INCPATH directory in the SConscript environment is converted to a --search flag for occbuild (line 113-115). So when libsrc/SConscript sets INCPATH='.', the libsrc/ directory ends up in the search path.
Autoconf (m4/occbuild.m4): The OCCAM_PROVIDE macro (occbuild.m4:218) registers modules in a file called $KROC_BUILD_ROOT/in-tree-modules. Each line maps a module name to its directory:
course.module /path/to/modules/course/libsrc
shared_screen.module /path/to/modules/course/libsrc course.module
When occbuild runs with --in-tree, it reads this file to discover where each dependency's .module file lives and adds those directories to the search path.
The Complete Chain
hello_world.occ
│ #INCLUDE "course.module"
▼
Build system (SCons) invokes:
occbuild --search /path/to/libsrc --need course --object hello_world.occ
│
▼
occbuild:
1. Builds search_path from --search flags, in-tree-modules, ISEARCH env
2. Sets ISEARCH="/path/to/libsrc:/path/to/forall:..."
3. Invokes occ21 (the C compiler)
│
▼
occ21:
1. Parses #INCLUDE "course.module"
2. Calls popen_relative("course.module", "ISEARCH", ...)
3. Reads ISEARCH env var, searches each directory
4. Finds and opens /path/to/libsrc/course.module
5. Parses it as occam source (it's a generated .occ-like file)
Key Insight
Your hunch was partly right -- Python scripts are involved, but they don't dynamically discover source files at include-resolution time. Instead, occbuild is invoked at build time to (a) generate the .module file from the library's declared dependencies, and (b) set up the ISEARCH environment so the C compiler can find it. The SConscript files orchestrate which sources go into each library and what flags get passed to occbuild. The compiler itself is just doing a simple path-based file search.