9.5 KiB
9.5 KiB
Learnings — tofu-core
Conventions, patterns, and successful approaches discovered during work on this plan.
Auto-scaffolded by /start-work. Append new entries below - never overwrite.
Task 5 — tofu.vercmp (RPM-style version comparison)
Module naming
versionis a D reserved keyword (conditional compilation). Module is namedtofu.vercmp, filesrc/tofu/vercmp.d. TheDepConstraintstruct intypes.duses fieldver(notversion) for the same reason.
Algorithm (exact port from ZETA/lib/vercmp.lua)
nextSegment: skips any non-digit/non-letter chars (separators); then reads a contiguous run of same-type chars. Returns(segment, nextIndex).cmpNumeric: strips leading zeros from both segments ("007"→"7","000"→"0"). Longer stripped string wins; then lexicographic.compare: strips all whitespace, then loops over alternating segments. If both segments are digit-typed →cmpNumeric, else lexical. When one version exhausts segments: the exhausted one is older (shorter = older). This matches RPM semantics:compare("1.0.0", "1.0") > 0.satisfies: usesfinal switchonDepOpenum. Maps tocompareresult as expected (ge→c>=0, le→c<=0, eq→c==0, ne→c!=0, gt→c>0, lt→c<0).parseDep: delegates toDepConstraint.parse(already ported in types.d by task 4). Both implementations are kept consistent.
whitespace stripping
- Could not use
filter+arraybecause Phobos auto-decodesstringtodcharrange elements, returningdchar[]notstring. Implemented a manualremoveWhitespacehelper using raw code-unit indexing, with a@trustedcast since the freshly-allocatedchar[]has no aliasing.
Test porting
- All 10 ZETA test categories ported as separate
@safe unittestblocks. - 4 modules pass unittests (types, vercmp, plus pre-existing ones).
dub buildpasses with warnings-as-errors enabled.
Future considerations
- When task 12 (constraint resolution) imports
tofu.vercmp, thecompareandsatisfiesfunctions are ready. TheparseDepintegration withDepConstraint.parseensures consistency between ZETA and tofu's dep constraint semantics.
Task 2: Config Loading (src/tofu/config.d)
toml package API (v1.0.0, Kripth/toml)
- Import:
import toml;→ providesTOMLDocument,TOMLValue,parseTOML,TOMLException,TOMLParserException TOMLDocumenthasalias table this— acts likeTOMLValue[string]AA- Access sections:
doc["core"]returnsTOMLValuewithtype == TOML_TYPE.TABLE - Access string values:
doc["core"]["key"].str(throws if type mismatch) - Check existence:
"key" in docreturnsTOMLValue*(null if absent) parseTOML()is@system— must wrap in@trustedblock when calling from@safecode- Keys without
.str/.integeraccess will throwTOMLException
D language gotchas
- Nested function declarations: D does NOT allow
void foo() { ... }inside a function body. Use lambdas:auto foo = () { ... }; foo();or anonymous() @trusted { ... }(); versionis a keyword: Cannot usestring version;as field name — usestring version_;or initialize with= ""parseTOMLis@system: The toml package's parse function isn't marked@safe— must wrap in@trustedlambdastd.file.readTextis@system: Same — wrap in@trustedstd.process.environment.get(): Returns empty string when env var is unset;"VAR" in environmentchecks existence butenvironment.get("VAR", "default")is cleanerstderr.writeflnis@safein recent DMD (2.106+) — usable directly in@safecode- AA
inoperator: Returns pointer (V*) ornull— must check for null before dereferencing
Pattern: env-overrideable config loading
- Priority: env vars > TOML file > hardcoded defaults
- TOML read happens BEFORE env override — so TOML values serve as base, env vars overwrite
- Missing config file → no error, just skip to defaults
- Malformed TOML →
TOMLParserExceptioncaught, warning to stderr, fall back to defaults - Testability:
load()accepts optionalenvOverridesmap and explicitconfigFilepath to avoid real env/filesystem
Config design decisions
TOFU_CONFIGenv var overrides default config file path (~/.config/tofu/config.toml)~in default paths expanded withstd.path.expandTilde- All path fields default to empty string when "find on PATH" — no hardcoded binary paths
defaultJobstyped asint(notsize_t/ulong) since it's a user-facing count
buildOptions warningsAsErrors deprecation
- DUB warns about
warningsAsErrorsinbuildOptions— recommendsbuildRequirementsinstead - Not blocking, informational only
Task 3 — tofu.log (colored, NO_COLOR-aware logging)
- dmd 2.112 gotcha: the
std.stdioglobalsstdout/stderrare@systemto access —makeGlobaluses__gshared File result, which fails@safeinference. An@safefunction cannot callstdout.writeln(...)directly. Fix: route every write through a tiny@trustedhelper (writeStdout(string)/writeStderr(string)), keep the public log functions@safe. Compiler hint: "using__gsharedinstead ofsharedmakes it fail to infer@safe". std.process.environment(dmd 2.112): it is anabstract final classof static methods —environment.get(name)is@safeand returnsnullwhen the var is unset (so "NO_COLOR set" ==get !is null, even for an empty string).environment["K"] = vandenvironment.remove(name)are@trusted. NOT deprecated in 2.112.- Testable color decision: make
colorEnabled()re-read the environment on every call instead of caching instatic this(). Enables NO_COLOR/TERM tests without restarting the process. Costs onegetenvper log line — negligible. - Env save/restore in unittests: a
private struct ColorEnv(inversion(unittest)) that snapshots NO_COLOR+TERM in its ctor and restores them in~this()(destructor) —auto env = ColorEnv("1", "xterm")scopes the restore.nullvalue = remove the var. Double-destruction from a copied temporary is harmless (restore is idempotent). - Capturing stdout/stderr in unittests: swap the global
stdout/stderrFile to aFile(name, "w+"), write,flush(), swap back, thenrewind()+readln(). Restore the global before the temp File's destructor runs. - zeta log format details:
warnpaints the whole prefix yellow (paint("yellow", "warn ") ~ msg), buterrorpaints only the word then" " ~ msg(paint("red","error") ~ " " ~ msg). Assertion must expect\x1b[31merror\x1b[0m failed deploy— the reset code sits between "error" and the message. versionis a D keyword — cannot be used as a struct field / identifier (trap for the paralleltypes.dwork).std.stdio.writelnflushes per call — already satisfies "no buffering"; no explicitflush()needed in production paths.- Verifying a single module without the full build:
dmd -main -unittest -i src/tofu/log.druns just that module's unittests when sibling modules (config.d,types.d) are mid-edit by parallel agents.-ipulls imports in automatically.
Task 4 — tofu.types (core data structures)
versionis a D keyword — all struct fields namedversionmust be renamed tover. The D compiler treatsversionas a conditional-compilation directive and rejects it as a field name (see task 3 learnings above).- Struct field defaults in D:
stringfields initialise tonullby default. To enforce empty-string semantics (""), every string field must be explicitly= "". final switchfor exhaustive enum handling: usingfinal switchon enum types forces the compiler to verify all cases are covered — catches missing branches at compile time. Used forpoolToString().- Manual character scanning preferred over
std.regex: the dep-spec parser uses simple char-by-char scanning withisNameChar()/isWhite()helpers instead of regex. This avoids thestd.regexdependency, keeps the parser@safe, and matches the Lua reference semantics exactly. - Dep spec parsing order matters: 2-character operators (
>=,<=,==,~=) MUST be checked before 1-character operators (>,<,=) to avoid false matches. The Lua reference uses an ordered tableOPS = { ">=", "<=", "==", "~=", ">", "<", "=" }— we replicate this with if/else chains. =normalises to==: the Lua code hasif op == "=" then op = "==" end; we map both toDepOp.eq.- Trailing garbage detection: after extracting version chars, any non-whitespace remaining in the spec string causes a
TypesException. The Lua pattern uses$anchor for this. dub testincludes all.dfiles in source paths for executable targets, not just imported modules. Pre-existing compile errors in sibling modules (config.d) block test runs. Fixed config.d by removingprivatefrom a nested function (access specifiers not allowed on local functions in D).BuildResult.failedasBuildFailure[]: the plan describes it asstruct {string name; string reason}[]— in D this is a named structBuildFailurewith array fieldBuildFailure[] failed. ThefailedNames()helper extracts just the names for caller convenience.DepConstraint.parseis a static factory method: returns a newDepConstraintby value, never allocates on the heap.@safethroughout.- All structs are
@safe: no methods do I/O, no@systemcalls. The module stands alone — no imports from other tofu modules.