+++ /dev/null
-<!DOCTYPE html>
-<html lang="en">
- <head>
- <meta charset="UTF-8">
- <title>C single file modules and namespaces</title>
- <script src="setup.js"></script>
- <script>
- window.StyleRT.include('RT/theme');
- window.StyleRT.include('RT/layout/article_tech_ref');
- </script>
- </head>
- <body>
- <RT-article>
- <RT-title
- author="Thomas Walker Lynch"
- date="2026-03-10"
- title="C single file modules and namespaces">
- </RT-title>
-
- <RT-TOC level="1"></RT-TOC>
-
- <h1>Single file C source</h1>
- <p>
- The RT C-culture abandons the traditional separation of declarations into <RT-code>.h</RT-code> header files and definitions into <RT-code>.c</RT-code> source files. Instead, a developer integrates the interface and the implementation into a single file.
- </p>
- <p>
- When a person includes the file using an <RT-code>#include</RT-code> directive, the file exposes only its interface. When the build system compiles the file into an object, a preprocessor macro acts as an implementation gate to compile the definitions. This keeps the source tree clean and ensures the interface and implementation never fall out of synchronization.
- </p>
-
- <h1>Ad hoc namespaces</h1>
- <p>
- The C language lacks native namespaces. To prevent symbol collisions in large projects, RT code format uses a center dot (<RT-code>·</RT-code>) to denote ad hoc namespaces within identifiers.
- </p>
- <p>
- A programmer maps the directory structure directly to these namespaces. For example, a file located at <RT-code>authored/ExampleGreet/Math.lib.c</RT-code> belongs to the <RT-code>ExampleGreet</RT-code> namespace and defines the <RT-code>Math</RT-code> module.
- </p>
- <p>
- Types and modules use <RT-code>PascalCase</RT-code>, while functions and variables use <RT-code>snake_case</RT-code>. An exported function from this module carries the full namespace and module prefix, such as <RT-code>ExampleGreet·Math·add</RT-code>.
- </p>
-
- <h1>The implementation gate</h1>
- <p>
- To achieve the single-file source pattern, the code relies on two preprocessor constructs. The entire file is wrapped in an include guard using the <RT-code>·ONCE</RT-code> suffix. This prevents redeclaration errors if the file is included multiple times.
- </p>
- <p>
- The definitions are wrapped in a single <RT-code>#ifdef</RT-code> block using the exact namespace and module name. The build system dynamically injects this macro via a <RT-code>-D</RT-code> compiler flag when building the module's specific object file.
- </p>
-
- <h2>Example: Math.lib.c</h2>
- <p>
- The following example demonstrates the complete structure.
- </p>
-
- <RT-code>
- #ifndef ExampleGreet·Math·ONCE
- #define ExampleGreet·Math·ONCE
-
- int ExampleGreet·Math·add(int a ,int b);
-
- #ifdef ExampleGreet·Math
-
- int ExampleGreet·Math·add(int a ,int b){
- return a + b;
- }
-
- #endif // ExampleGreet·Math
- #endif // ExampleGreet·Math·ONCE
- </RT-code>
-
- <h2>Build system mechanics</h2>
- <p>
- When a consumer file, such as <RT-code>hello.CLI.c</RT-code>, contains <RT-code>#include "Math.lib.c"</RT-code>, the compiler processes the file without the <RT-code>ExampleGreet·Math</RT-code> macro defined. It skips the implementation and reads only the function prototype.
- </p>
- <p>
- When the orchestrator compiles the library object, it evaluates the target name and explicitly passes <RT-code>-DExampleGreet·Math</RT-code> to the compiler. This unlocks the gate, compiling the machine code for the definitions into <RT-code>Math.lib.o</RT-code>.
- </p>
-
- </RT-article>
- </body>
-</html>
--- /dev/null
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <title>Single-file C modules and namespaces</title>
+ <script src="setup.js"></script>
+ <script>
+ window.StyleRT.include('RT/theme');
+ window.StyleRT.include('RT/layout/article_tech_ref');
+ </script>
+ </head>
+ <body>
+ <RT-article>
+ <RT-title
+ author="Thomas Walker Lynch"
+ date="2026-03-10"
+ title="Single-file C modules and namespaces">
+ </RT-title>
+
+ <RT-TOC level="1"></RT-TOC>
+
+ <h1>Single-file C source</h1>
+ <p>
+ The RT C language culture abandons the traditional separation of declarations into <RT-code>.h</RT-code> header files and definitions into <RT-code>.c</RT-code> source files. A developer integrates the interface and the implementation into a single file.
+ </p>
+ <p>
+ When a person includes the file using an <RT-code>#include</RT-code> directive, the file exposes only its interface. When the build system compiles the file into an object, a preprocessor macro acts as an implementation gate to compile the definitions. This keeps the source tree clean and ensures the interface and implementation never fall out of synchronization.
+ </p>
+
+ <h1>Ad hoc namespaces</h1>
+ <p>
+ The C language lacks native namespaces. To prevent symbol collisions in large projects, RT code format uses a center dot (<RT-code>·</RT-code>) to denote ad hoc namespaces within identifiers.
+ </p>
+ <p>
+ A programmer maps the directory structure directly to these namespaces. For example, a file located at <RT-code>authored/ExampleGreet/Math.lib.c</RT-code> belongs to the <RT-code>ExampleGreet</RT-code> namespace and defines the <RT-code>Math</RT-code> module.
+ </p>
+ <ul>
+ <li>Types and modules use <RT-code>PascalCase</RT-code>.</li>
+ <li>Functions and variables use <RT-code>snake_case</RT-code>.</li>
+ </ul>
+ <p>
+ An exported function from this module carries the full namespace and module prefix, such as <RT-code>ExampleGreet·Math·add</RT-code>.
+ </p>
+
+ <h1>The implementation gate</h1>
+ <p>
+ To achieve the single-file source pattern, the code relies on two preprocessor constructs:
+ </p>
+ <ul>
+ <li><strong>Include Guard:</strong> The entire file is wrapped in an include guard using the <RT-code>·ONCE</RT-code> suffix. This prevents redeclaration errors if the file is included multiple times.</li>
+ <li><strong>Implementation Block:</strong> The definitions are wrapped in a single <RT-code>#ifdef</RT-code> block using the exact namespace and module name. The build system dynamically injects this macro via a <RT-code>-D</RT-code> compiler flag when building the module's specific object file.</li>
+ </ul>
+
+ <h2>Build system mechanics</h2>
+ <p>
+ When a consumer file, such as <RT-code>hello.CLI.c</RT-code>, contains <RT-code>#include "Math.lib.c"</RT-code>, the compiler processes the file without the <RT-code>ExampleGreet·Math</RT-code> macro defined. It skips the implementation and reads only the function prototype.
+ </p>
+ <p>
+ When the orchestrator compiles the library object, it evaluates the target name and explicitly passes <RT-code>-DExampleGreet·Math</RT-code> to the compiler. This unlocks the gate, compiling the machine code for the definitions into <RT-code>Math.lib.o</RT-code>.
+ </p>
+
+ <h2>Example: Math.lib.c</h2>
+ <p>
+ The following example demonstrates the complete structure.
+ </p>
+
+ <RT-code>
+ #ifndef ExampleGreet·Math·ONCE
+ #define ExampleGreet·Math·ONCE
+
+ int ExampleGreet·Math·add(int a ,int b);
+
+ #ifdef ExampleGreet·Math
+
+ int ExampleGreet·Math·add(int a ,int b){
+ return a + b;
+ }
+
+ #endif // ExampleGreet·Math
+ #endif // ExampleGreet·Math·ONCE
+ </RT-code>
+
+ <h2>Cross-module dependencies</h2>
+ <p>
+ When one module depends on another, the developer directly includes the library source file. For example, if the Greeter module requires the Math module, the file <RT-code>Greeter.lib.c</RT-code> will contain:
+ </p>
+ <RT-code>
+ #include "Math.lib.c"
+ </RT-code>
+ <p>
+ Because every file is protected by a <RT-code>·ONCE</RT-code> include guard, it is safe for multiple modules to include the same dependency. The preprocessor will only expand the interface once per translation unit.
+ </p>
+
+ <h2>Information hiding</h2>
+ <p>
+ To define internal helper functions or private data that should not be exposed in the module's public interface, a programmer places them strictly inside the <RT-code>#ifdef</RT-code> implementation block.
+ </p>
+ <p>
+ To prevent these internal symbols from leaking into the global namespace during linking, they must be given internal linkage. The RT skeleton provides the <RT-code>Local</RT-code> macro (defined as <RT-code>static</RT-code> in <RT-code>RT_global.h</RT-code>) for this exact purpose.
+ </p>
+ <RT-code>
+ #ifdef ExampleGreet·Math
+
+ Local int internal_helper(int val){
+ return val * 2;
+ }
+
+ int ExampleGreet·Math·add(int a ,int b){
+ return internal_helper(a) + b;
+ }
+
+ #endif // ExampleGreet·Math
+ </RT-code>
+
+ <h2>Executable entry points</h2>
+ <p>
+ Programs intended to be compiled into standalone executables use the <RT-code>.CLI.c</RT-code> suffix. These files consume the library modules but do not define their own namespaces or include guards, as they are never included by other files.
+ </p>
+ <p>
+ A <RT-code>.CLI.c</RT-code> file includes the necessary <RT-code>.lib.c</RT-code> files, parses command-line arguments in the <RT-code>main</RT-code> function, and passes native data types to a dedicated <RT-code>CLI()</RT-code> function to orchestrate the core logic.
+ </p>
+
+ </RT-article>
+ </body>
+</html>
#!/usr/bin/env -S python3 -B
# -*- mode: python; coding: utf-8; python-indent-offset: 2; indent-tabs-mode: nil -*-
-import os ,sys ,shutil ,stat ,pwd ,grp ,glob ,tempfile
+import os ,sys ,shutil ,stat ,pwd ,grp ,glob ,tempfile ,filecmp
HELP = """usage: promote {write|clean|ls|diff|help|dry write}
write Writes promoted files from scratchpad/made into consumer/made. Only updates newer files.
clean Remove all contents of the consumer/made directory.
ls List consumer/made as an indented tree: PERMS OWNER NAME.
- diff List files in consumer/made that are not in scratchpad/made, or are newer.
+ diff List missing, orphaned, or out-of-sync files between scratchpad and consumer.
help Show this message.
dry write Preview what write would do without modifying the filesystem.
"""
def assert_setup():
setup_val = os.environ.get("SETUP" ,"")
- if(setup_val != SETUP_MUST_BE):
+ if( setup_val != SETUP_MUST_BE ):
hint = (
"SETUP is not 'developer/tool/setup'.\n"
"Enter the project with: . setup developer\n"
except Exception: pass
def ensure_dir(path_str ,mode=DEFAULT_DIR_MODE ,dry=False):
- if(dry):
+ if( dry ):
if( not os.path.isdir(path_str) ):
shown = _display_dst(path_str) if path_str.startswith(consumer_root()) else (
os.path.relpath(path_str ,dev_root()) if path_str.startswith(dev_root()) else path_str
it = list(os.scandir(path_str))
except FileNotFoundError:
return
- dirs = [e for e in it if e.is_dir(follow_symlinks=False)]
- files = [e for e in it if not e.is_dir(follow_symlinks=False)]
- dirs.sort(key=lambda e: e.name)
- files.sort(key=lambda e: e.name)
-
- if(is_root):
- for f in ( e for e in files if e.name.startswith(".") ):
- st = os.lstat(f.path)
- entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,f.name))
- for d in dirs:
- st = os.lstat(d.path)
- entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,d.name + "/"))
- gather(d.path ,depth + 1 ,False)
- for f in ( e for e in files if not e.name.startswith(".") ):
- st = os.lstat(f.path)
- entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,f.name))
+ dirs = [TM_e for TM_e in it if TM_e.is_dir(follow_symlinks=False)]
+ files = [TM_e for TM_e in it if not TM_e.is_dir(follow_symlinks=False)]
+ dirs.sort(key=lambda TM_e: TM_e.name)
+ files.sort(key=lambda TM_e: TM_e.name)
+
+ if( is_root ):
+ for TM_f in ( TM_e for TM_e in files if TM_e.name.startswith(".") ):
+ st = os.lstat(TM_f.path)
+ entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name))
+ for TM_d in dirs:
+ st = os.lstat(TM_d.path)
+ entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_d.name + "/"))
+ gather(TM_d.path ,depth + 1 ,False)
+ for TM_f in ( TM_e for TM_e in files if not TM_e.name.startswith(".") ):
+ st = os.lstat(TM_f.path)
+ entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name))
else:
- for d in dirs:
- st = os.lstat(d.path)
- entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,d.name + "/"))
- gather(d.path ,depth + 1 ,False)
- for f in files:
- st = os.lstat(f.path)
- entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,f.name))
+ for TM_d in dirs:
+ st = os.lstat(TM_d.path)
+ entries.append((True ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_d.name + "/"))
+ gather(TM_d.path ,depth + 1 ,False)
+ for TM_f in files:
+ st = os.lstat(TM_f.path)
+ entries.append((False ,depth ,filemode(st.st_mode) ,owner_group(st) ,TM_f.name))
gather(root_dp ,1 ,True)
ogw = 0
- for _isdir ,_depth ,_perms ,ownergrp ,_name in entries:
- if( len(ownergrp) > ogw ):
- ogw = len(ownergrp)
+ for TM_isdir ,TM_depth ,TM_perms ,TM_ownergrp ,TM_name in entries:
+ if( len(TM_ownergrp) > ogw ):
+ ogw = len(TM_ownergrp)
print("consumer/made/")
- for isdir ,depth ,perms ,ownergrp ,name in entries:
- indent = " " * depth
- print(f"{perms} {ownergrp:<{ogw}} {indent}{name}")
+ for TM_isdir ,TM_depth ,TM_perms ,TM_ownergrp ,TM_name in entries:
+ indent = " " * TM_depth
+ print(f"{TM_perms} {TM_ownergrp:<{ogw}} {indent}{TM_name}")
def copy_one(src_abs ,dst_abs ,mode ,dry=False):
src_show = _display_src(src_abs)
parent = os.path.dirname(dst_abs)
os.makedirs(parent ,exist_ok=True)
- if(dry):
+ if( dry ):
if( os.path.exists(dst_abs) ):
print(f"(dry) unlink '{dst_show}'")
print(f"(dry) install -m {oct(mode)[2:]} -D '{src_show}' '{dst_show}'")
exit_with_status(f"cannot find developer scratchpad made at '{_display_src(src_root)}'")
wrote = False
- for root ,dirs ,files in os.walk(src_root):
- dirs.sort()
- files.sort()
- for fn in files:
- src_abs = os.path.join(root ,fn)
+ for TM_root ,TM_dirs ,TM_files in os.walk(src_root):
+ TM_dirs.sort()
+ TM_files.sort()
+ for TM_fn in TM_files:
+ src_abs = os.path.join(TM_root ,TM_fn)
rel = os.path.relpath(src_abs ,src_root)
dst_abs = os.path.join(cpath() ,rel)
if( os.path.exists(dst_abs) ):
- src_mtime = os.stat(src_abs).st_mtime
- dst_mtime = os.stat(dst_abs).st_mtime
- if(dst_mtime >= src_mtime):
+ # Use content comparison instead of timestamps
+ if( filecmp.cmp(src_abs ,dst_abs ,shallow=False) ):
continue
st = os.stat(src_abs)
,"made"
)
- if( not os.path.isdir(dst_root) ):
- print(f"Consumer made directory {_display_dst(dst_root)} does not exist.")
+ src_files = set()
+ if( os.path.isdir(src_root) ):
+ for TM_root ,TM_dirs ,TM_files in os.walk(src_root):
+ for TM_fn in TM_files:
+ src_files.add(os.path.relpath(os.path.join(TM_root ,TM_fn) ,src_root))
+
+ dst_files = set()
+ if( os.path.isdir(dst_root) ):
+ for TM_root ,TM_dirs ,TM_files in os.walk(dst_root):
+ for TM_fn in TM_files:
+ dst_files.add(os.path.relpath(os.path.join(TM_root ,TM_fn) ,dst_root))
+
+ if( not src_files and not dst_files ):
+ print("No differences found. Both directories are empty or missing.")
return
+ all_files = sorted(list(src_files | dst_files))
found_diff = False
- for root ,dirs ,files in os.walk(dst_root):
- dirs.sort()
- files.sort()
- for fn in files:
- dst_abs = os.path.join(root ,fn)
- rel = os.path.relpath(dst_abs ,dst_root)
- src_abs = os.path.join(src_root ,rel)
-
- if( not os.path.exists(src_abs) ):
- print(f"Orphaned in consumer made: {rel}")
- found_diff = True
- else:
- dst_mtime = os.stat(dst_abs).st_mtime
+
+ for TM_rel in all_files:
+ if( TM_rel not in dst_files ):
+ print(f"Pending promotion (missing in consumer made): {TM_rel}")
+ found_diff = True
+ elif( TM_rel not in src_files ):
+ print(f"Orphaned in consumer made (missing in scratchpad): {TM_rel}")
+ found_diff = True
+ else:
+ src_abs = os.path.join(src_root ,TM_rel)
+ dst_abs = os.path.join(dst_root ,TM_rel)
+
+ # Compare contents first
+ if( not filecmp.cmp(src_abs ,dst_abs ,shallow=False) ):
src_mtime = os.stat(src_abs).st_mtime
- if(dst_mtime > src_mtime):
- print(f"Newer in consumer made: {rel}")
- found_diff = True
+ dst_mtime = os.stat(dst_abs).st_mtime
+
+ # If they differ, check timestamps to infer direction
+ if( src_mtime > dst_mtime ):
+ print(f"Pending update (contents differ, newer in scratchpad): {TM_rel}")
+ else:
+ print(f"Contents differ (locally modified in consumer made): {TM_rel}")
+ found_diff = True
if( not found_diff ):
print("No differences found. Consumer made matches developer scratchpad made.")
+
def cmd_clean():
assert_setup()
consumer_root_dir = cpath()
if( not os.path.isdir(consumer_root_dir) ):
return
- for name in os.listdir(consumer_root_dir):
- p = os.path.join(consumer_root_dir ,name)
+ for TM_name in os.listdir(consumer_root_dir):
+ p = os.path.join(consumer_root_dir ,TM_name)
if( os.path.isdir(p) and not os.path.islink(p) ):
shutil.rmtree(p ,ignore_errors=True)
else:
if( len(sys.argv) < 2 ):
print(HELP)
return
- cmd ,*args = sys.argv[1:]
- if(cmd == "write"):
+
+ cmd = sys.argv[1]
+ args = sys.argv[2:]
+
+ if( cmd == "write" ):
cmd_write(dry=False)
- elif(cmd == "clean"):
+ elif( cmd == "clean" ):
cmd_clean()
- elif(cmd == "ls"):
+ elif( cmd == "ls" ):
list_tree(cpath())
- elif(cmd == "diff"):
+ elif( cmd == "diff" ):
cmd_diff()
- elif(cmd == "help"):
+ elif( cmd == "help" ):
print(HELP)
- elif(cmd == "dry"):
+ elif( cmd == "dry" ):
if( args and args[0] == "write" ):
cmd_write(dry=True)
else: