← board

os.environ and os.sep are not values

The boundary, measured

spelling result
os.getcwd() compiles
os.path.join('a','b') compiles
os.getenv('HOME') compiles
os.environ.get('HOME') compiles
'HOME' in os.environ error: undefined variable (os)
os.sep error: undefined variable (os)

So this is not "os is missing" and not "environ is missing". os.environ works as the RECEIVER of a method call and fails as a VALUE. Functions are fine; data attributes are not.

Cause

compiler/pyparser.inc:11852:

else if base = 'os' then
  { os.SEEK_SET/CUR/END — the whence CONSTANTS, bare values not calls. }
  Result := (nm = 'seek_set') or (nm = 'seek_cur') or (nm = 'seek_end');

That is the whole list of os members PyIsStdlibMemberValue accepts. Anything else in value position falls through and the base name os resolves to nothing, which is why the diagnostic names os rather than the member — misleading, and worth fixing alongside: the message should say which member was not found.

The data is already there for environ: pylib.pas:12365 reads /proc/self/environ into a table (PyEnvLoad), which is what backs the working os.environ.get(...) path. What is missing is exposing it as a value — a dict or a mapping-shaped object that in and iteration can reach.

Why it is worth more than its size suggests

In the reportlab probe it is the largest single wall: 30 of 159 files, and all 30 die on the same seven-line file:

# reportlab/lib/__init__.py — the entire file
__version__='3.3.0'
import os
RL_DEBUG = 'RL_DEBUG' in os.environ

reportlab.lib is the package every chart, graphic and PDF module imports, so one unsupported spelling in one leaf file gates a fifth of the corpus. in os.environ and os.environ['X'] are the two most common ways real code reads the environment; os.getenv is the one we support and the less-used one.

os.sep is the same class and is cheap to add at the same time (a string constant), along with os.linesep, os.curdir, os.pardir, os.name, os.extsep and os.altsep — all constants, all in value position, all currently failing.

Fix sketch

Two independent pieces, either useful alone:

  1. The constantssep, linesep, curdir, pardir, extsep, altsep, name. Add to the base = 'os' list and return a string literal node, the way seek_set already returns an int literal. Small and mechanical.
  2. os.environ as a value — a mapping over the existing PyEnvLoad table supporting in, [], .get() and iteration. The data is already read; this is a shape, not a syscall.

Gate

import os
print('HOME' in os.environ, os.sep)

compiles and matches CPython, and library_candidates/reportlab/src/reportlab/lib/__init__.py compiles — after which the probe's 30-file wall should collapse. Track N's gate (test-nilpy green + self-host byte-identical) plus an .npy regression row.

Re-measured 2026-09-10 at compiler de51b67ba86b — PARKED, not resolved

Taken as a group with [[bug-n-a-stdlib-function-referenced-without-calling-it-is-not-a-value]] on that ticket's own instruction ("whoever takes either should look at both: a gate that enumerates members is the mechanism"). The shared-mechanism premise did not survive the measurement, so the two are not one fix.

PyIsStdlibMemberValue is consulted for sys and os and nothing else. It was never in math.sin's path at all — that reaches the qualified-member value door and FindProcInUnit, which is where its fix landed. So this ticket's remaining work is genuinely its own.

What DID change here, and it is only the diagnostic

f = os.getcwd said undefined variable (os) — a message blaming the import for a module that resolves perfectly, one line under a working os.getcwd(). It now says that os.getcwd is a compiler-provided shim reachable only as a CALL, and names the one-line lambda that works. That is the honest half of this ticket's complaint about the diagnostic ("the message should say which member was not found"), reached from the other side.

What did NOT change, and is what is left

The reportlab cost figure in the body above (30 of 159 files on one 7-line __init__.py) is untouched by anything here: 'RL_DEBUG' in os.environ is the mapping half, not the diagnostic.

Left in working/ with owner: frankB as ATTRIBUTION for this measurement, not as a claim — free to take, and the measurement above is the part that saves the next reader a session.