# reef-lang Feedback — Surfaced While Planning repoman v0.1

**Source:** Spec'ing `repoman` (a real Linux CLI tool: Incus container management + rsync/NFS backup) against reef-lang 0.5.9 stdlib. Items below are gaps and quirks discovered while mapping the spec onto actual stdlib APIs. None are blockers — repoman v0.1 will work around them — but each is a friction point a future user will hit when writing a real production reef program.

---

## 1. Encoders missing for structured formats — **highest impact**

Every config-driven program eventually needs to **write** its config, not just read it. Today:

| Module | Reads | Writes |
|---|---|---|
| `encoding.toml` | yes (`toml_parse`, `toml_get*`, `toml_array_*`) | **no** |
| `encoding.json` | yes (`json_parse`, `json_get*`) | **no** |
| `encoding.yaml` | yes | only line-at-a-time `yaml_encode_string/int/bool` — no nested tables, no lists |
| `encoding.ini` | yes (line helpers) | **no** |
| `encoding.csv` | TBD | TBD |

**What repoman needs:** a `toml_serialize(...)` (or equivalent for one of the structured formats) that handles strings, ints, bools, string arrays, tables, and table-arrays (`[[project]]`).

**Suggested API shape** (TOML; same idea for JSON):
```
fn toml_serialize(keys: [string], values: [string], count: int): string
// or a higher-level builder:
type TomlBuilder
fn toml_builder(): TomlBuilder
proc toml_set_string(b: TomlBuilder, key: string, val: string)
proc toml_set_int(b: TomlBuilder, key: string, val: int)
proc toml_set_bool(b: TomlBuilder, key: string, val: bool)
proc toml_set_string_array(b: TomlBuilder, key: string, vals: [string])
proc toml_array_append_table(b: TomlBuilder, name: string)  // for [[project]]
fn toml_render(b: TomlBuilder): string
```

Parser/serializer asymmetry is the single biggest gap stdlib has for "write a real config-driven program."

## 2. `io.file` is missing rename and fsync

`io.file` exposes `readFile`, `writeFile`, `appendFile`, `deleteFile`, `fileExists`, `fileSize`, plus binary versions. **Missing:**
- `rename(old, new)` — `fs.ops.rename` exists, so this is just a discoverability issue (people will look in `io.file` first).
- `fsync(path)` — there is **no** way to force a file's contents to disk before a rename. Important for the standard atomic-write recipe (write tmp → fsync → rename). Today repoman has to skip the fsync and accept a brief power-loss inconsistency window.

**Suggested:** add `fn fsync(path: string): bool` (or expose a file-handle-based API, then add `flush_to_disk(handle)`).

## 3. No generic `Result[T, E]`

`core.result` only exports concrete `ResultInt`, `ResultStr`, `ResultBool`, `ResultFloat`. For functions that return structs (e.g., `load_registry() : Result<Registry, string>`), every caller has to define a local enum:
```reef
type RegistryLoad = enum
    Ok(Registry)
    Err(string)
end RegistryLoad
```

`core.option_generic` already provides `Option[T]` — the same pattern would be a clean fit for `Result[T, E]`. The existing concrete `ResultInt`/`ResultStr`/etc. could be deprecated aliases or stay as ergonomic shortcuts.

**Suggested:** add `core.result_generic` with `Result[T, E]`, `is_ok[T,E]`, `is_err[T,E]`, `unwrap_ok[T,E]`, `unwrap_err[T,E]`.

## 4. `io.path` is missing `expand_home`

`~/foo` expansion is one of the most frequent path operations in any user-facing CLI. Today `io.path` has `join_path`, `dirname`, `basename`, `extension`, `normalize`, `is_absolute` — but no `expand_home` or `expand_user`. Every caller writes (corrected from v1: `str.substring_from` does not exist publicly; the real call is `str.substring(s, start, len)`):
```reef
fn expand_home(p: string): string
    if str.starts_with(p, "~/")
        let len = str.length(p)
        return path.join_path(env.get_env_or("HOME", ""), str.substring(p, 2, len - 2))
    end if
    return p
end expand_home
```

**Suggested:** `fn expand_home(path: string): string` (and maybe `expand_user(path)` for `~user/...`) in `io.path`.

## 5. `sys.flag` doesn't support subcommand dispatch

`sys.flag` is a V-style **flat** flag parser. It calls `sys.args` directly inside `parse()` — there's no way to feed it a subcommand-sliced argv. The common CLI shape `tool <subcommand> [subcommand-flags]` (git, kubectl, incus, podman, repoman) requires a hand-rolled outer parser to extract the subcommand, then either a hand-rolled per-subcommand parser or a way to give `sys.flag` a sub-slice of argv.

**Suggested:** either
- (a) `flag_parser_from(args: [string]): FlagParser` so callers can pass a sliced argv, or
- (b) a `subcommand_parser` API at the same level as `flag_parser` that handles the dispatch.

Even a doc note ("for subcommand tools, hand-roll using `sys.args.get(i)`") would help — today the example in `sys/flag.reef` makes it look like the answer.

## 6. `test.framework` thin assertion surface

`TestRunner` (an active object — nice) already has `assert_eq_int/bool/string/float`, and **as of v0.5.9 also has** `assert_true`, `assert_false`, and `assert_not_eq_int/bool/string` (filer error in v1 of this report — caught reading only the first 100 lines of a 235-line file). **Genuinely missing:**
- `assert_contains(haystack, needle, message)` for substring/array membership.
- `assert_ok_*` / `assert_err_*` for Result types (uses pattern-match plus a diagnostic that surfaces the wrapped error/value on failure).

## 7. TOML parser quirks

- **Hard 1024-entry cap** in `toml_alloc_keys()`. Adequate for repoman (~127 projects max), but the cap is invisible — `toml_parse` returns the entry count it processed; if the file had more, callers don't know. Suggest a return value indicating "truncated" (or accept a max-entries arg and error if exceeded — the `_sized` variants already do the latter, so making them the recommended path would help).
- **Parallel-array repr** (`[string] keys`, `[string] values`, `int count`) forces every caller to thread three values around. A small `TomlDoc` struct (or `[Pair]` array) would be friendlier — the runtime cost is the same but the API surface is much smaller.

## 8. Naming nit

`io.path.join_path(a, b)` — the `_path` suffix is redundant since the function is in `io.path`. Most languages call this `path.join`. Minor, but discoverability matters when learning a new stdlib.

---

## Summary table (ordered by impact on real-world reef programs)

| # | Item | Severity | Type |
|---|---|---|---|
| 1 | TOML/JSON encoders missing | **High** | Feature gap |
| 2 | `io.file` no rename/fsync | High | Feature gap |
| 3 | No generic `Result[T,E]` | Medium | API design |
| 4 | `io.path.expand_home` missing | Medium | Feature gap |
| 5 | `sys.flag` no subcommand support | Medium | API design |
| 6 | `test.framework` thin assertions | Medium | Feature gap |
| 7 | TOML parser caps & repr | Low | API design |
| 8 | `join_path` naming | Low | Naming |

repoman v0.1 works around all of these and ships. But every one of them costs the project some hand-rolled glue code that *should* be in stdlib.
