Skip to content

feat: language capability rows — assembler language-agnosticism (#619) - #643

Merged
apotema merged 3 commits into
mainfrom
feat/619-language-rows
Jul 19, 2026
Merged

feat: language capability rows — assembler language-agnosticism (#619)#643
apotema merged 3 commits into
mainfrom
feat/619-language-rows

Conversation

@apotema

@apotema apotema commented Jul 19, 2026

Copy link
Copy Markdown
Contributor

Opens the assembler's language vocabulary so a language plugin can add a brand-new language (the RFC's "python") through a plugin.labelle .languages capability row alone — zero assembler changes. This closes the last per-language hardcode the already-shipped row-driven declare/transpile/embed/native machinery (runGenericDeclarePhase, spliceFromRow; #621#624) sat on top of.

Part of labelle-toolkit/labelle-engine#237; RFC-LANGUAGE-PLUGINS rev 16 §7 "the language-agnostic assembler".

What already existed vs what this PR adds

Already on main (not rebuilt here):

  • Row-driven declare (runGenericDeclarePhase), embed/native/transpile splice (spliceFromRow) — ts/crystal already ride manifest rows.
  • The frozen DECLARE_RUNNERS / EMBED_LANGUAGES / NATIVE_LANGUAGES / TSC_PLATFORMS tables demoted to fallback defaults for row-less (pre-migration) manifests.
  • The LanguageRow manifest schema (.name/.extensions/.kind/.module_root/.stage_subdir/.declare/.transpile).

The gap this PR fills — the language VOCABULARY axis. Before this PR, the closed SUPPORTED_LANGUAGES / scriptExtensions table was consulted by the policy gate, the shared-dir scan, the plugin-event consumption scan, and requires_language validation, and it rejected any language it had never heard of before the row machinery ever ran — so the litmus was impossible. Now:

  • language_policy.Vocabulary — the project's OPEN language set: frozen built-ins ∪ the declaring scripting plugin's manifest .languages rows. Rows are PRIMARY (a row shadows a frozen language's extensions); a row language the tables never heard of is admitted like a built-in. EMPTY (no rows) reproduces pre-Language capability rows in plugin.labelle — make the assembler language-agnostic #619 behavior bit-for-bit.
  • validateDeclaredLanguage — the vocabulary gate, re-homed from resolveProjectLanguage into generate_phases.validateLanguagePolicy where the manifest rows are in hand. Self-describing capabilities replace the closed table: a pointed error naming both built-ins and manifest rows, no version compare.
  • resolveProjectLanguage / manifest-load requires_language now gate on SHAPE only (isLanguageIdentifier[a-z][a-z0-9_]*, the one constraint the generated .language = .<name> enum literal genuinely imposes).
  • scanUnitLanguageDirs / checkRequiresLanguage take the Vocabulary and police row languages by the row's extensions.
  • plugin-event consumption scan gains the active splice's script extensions (filterConsumedEvents(extra_extensions)) so a row language's .py sources are not invisible to the scan and its subscriptions silently elided.

Migration map (hardcoded path → row-driven)

Hardcoded path Now
closed SUPPORTED_LANGUAGES check in resolveProjectLanguage Vocabulary.isKnown (frozen ∪ rows) in validateDeclaredLanguage
scriptExtensions-only dir/event policing Vocabulary.extensionsOf (row extensions primary, frozen fallback)
manifest-load closed-table requires_language reject shape-only reject + generate-time vocabulary check
declare / embed / native / transpile dispatch already runGenericDeclarePhase / spliceFromRow rows (unchanged); frozen tables stay fallbacks

Back-compat proof

LUA_ROW_BACKCOMPAT generates a lua project twice — frozen fallback (no row) vs an equivalent .languages row — and asserts the main.zig + build.zig are byte-identical. Migrating lua/ruby onto rows shifts nothing on output. Every existing golden/e2e suite is unchanged.

Litmus evidence

test/quokka_litmus_tests.zig — a fake embedded-VM language "quokka" (.qk) the assembler learns ENTIRELY from a manifest row:

  • generate end-to-end: scripting.registerScript("behavior", @embedFile("scripts/behavior.qk")), .language = .quokka dep arg, the alias + scripting_enabled flag, staged embed source.
  • a foreign frozen-language file (.rb) in the quokka project still errors ScriptLanguageMismatch — the combined (frozen ∪ row) vocabulary polices both ways.
  • META: "quokka" and ".qk" appear in NO src/**/*.zig — the mechanical proof the generate rode rows, not a hidden branch. (The fixture is named "quokka" rather than literally "python" precisely so this substring proof is unambiguous — "python" already appears in assembler source as an illustrative RFC comment.)

Coordination

Rebased over #641 (consumption-filter precision — resolved the Walk/filterConsumedEvents signature conflict, keeping both .gated_downgrade and the new .extra_extensions). Open siblings #639/#640/#642 don't touch the vocabulary/row layer. Merges LAST.

Deviation

The csharp dev-.csproj IDE aid (#617) still branches on s.language == "csharp" in root.zig — a dev convenience (not a generate-correctness path), analogous to the RFC's assembler-owned "honest boundary" codegen residue (tsconfig, d.ts sidecar). Not row-ified here; the embedded-language litmus is unaffected. Row-ifying it would need a .dev_project capability — out of scope for this PR.

Tests: full suite green except the pre-existing flow_catalog env-lane baseline (2 failures present on clean origin/main).

https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag /codesmith with what you need. Autofix is disabled.


Update — rebased onto current main + #644 csharp migration (final)

Rebased over the whole backlog sweep now on main (#640 debug, #645 showcase, #644 csharp EMBED, #642 pack-dirs watch, #639 sidecar prune). One conflict (build.zig test list) — kept both csharp_splice_tests.zig and quokka_litmus_tests.zig.

#644 (csharp EMBED) migration — what was already generic vs what I row-ified:

  • The EMBED compile wiring needed NO migration — already fully row-driven. csharp has NO frozen-table fallback; it resolves entirely through its manifest .languages native row (kind = .native, module_root/stage_subdir) + plugin-declared .language_builds dotnet publish. The actual scripts/*.cs compile rides spliceFromRow + stageNativeSources. Confirmed generic — left untouched.
  • Two csharp-NAMED decisions row-ified (feat: csharp EMBED — compile scripts/*.cs at generate (#617) #644's module doc explicitly anticipated this):
    • stagesRuntimeOutputs no longer hardcodes language == "csharp" — it consults a new LanguageRow.runtime_output capability (the link-less build outputs ARE the runtime payload → stage beside the binary + set the run-step assembly-dir env).
    • the ensureStepToolsOnPath call is gated on a new LanguageRow.probe_tools capability instead of dl == "csharp".
    • Both resolved by ScriptingSplice.detect (row PRIMARY ∪ frozen fallback) and threaded onto the splice. The csharp knowledge is demoted to a frozen fallback (scripting_csharp.frozenRuntimeOutput/frozenProbeTools) — the same Language capability rows in plugin.labelle — make the assembler language-agnostic #619 pattern as DECLARE_RUNNERS/NATIVE_LANGUAGES. A new runtime-loaded language declares .runtime_output/.probe_tools in its row and stages with zero assembler changes.
  • Documented residue (RFC honest boundary, not migrated): the runtime assembly-dir env var NAME (LABELLE_CS_ASSEMBLY_DIR) stays an assembler-owned constant, and the dev-.csproj IDE aid stays csharp-keyed MSBuild codegen — analogous to the assembler-owned tsconfig codegen. Neither is needed for the language-agnostic litmus (which adds an EMBEDDED language, not a runtime-loaded native one).

Back-compat proof: #644's csharp_splice_tests.zig e2e stays green unchanged — its fixture manifest carries NO .runtime_output/.probe_tools, yet csharp still stages runtime outputs + probes the toolchain via the frozen fallback. That IS the byte-identical migration proof for csharp. The quokka litmus + LUA_ROW_BACKCOMPAT byte-identity remain green.

Tests: runtime_output/probe_tools row parse (+ default-false), frozen-fallback (csharp opts in, other langs out), capability-driven stagesRuntimeOutputs. Full suite green except the pre-existing flow_catalog env-lane baseline (2 failures on clean origin/main).

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 36 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 6573800c-778e-42a4-b0b8-2390f4c93da7

📥 Commits

Reviewing files that changed from the base of the PR and between 62a4ef8 and 9149be1.

📒 Files selected for processing (11)
  • build.zig
  • src/build_files/build_zig.zig
  • src/codegen/scan/event_consumption.zig
  • src/language_policy.zig
  • src/plugin_manifest/pack.zig
  • src/plugin_manifest/plugin.zig
  • src/root.zig
  • src/root/generate_phases.zig
  • src/scripting_csharp.zig
  • src/scripting_splice.zig
  • test/quokka_litmus_tests.zig
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/619-language-rows

Comment @coderabbitai help to get the list of available commands.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request implements support for open language vocabularies (RFC-LANGUAGE-PLUGINS §7, issue #619), allowing the assembler to learn and support new script languages entirely from manifest capability rows rather than a closed, frozen list of built-ins. Load-time validation for language requirements is relaxed to shape-only checks, while full vocabulary validation is deferred to generate-time when the scripting plugin's manifest is available. Additionally, the event consumption scan is updated to include row-declared language extensions, and a comprehensive 'quokka' litmus test suite is added to verify end-to-end agnosticism. No review comments were provided, so there is no feedback to address.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1210adbe9

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/language_policy.zig
if (name[0] < 'a' or name[0] > 'z') return false;
for (name[1..]) |c| {
switch (c) {
'a'...'z', '0'...'9', '_' => {},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject language names that break build.zig emission

When a manifest row uses an identifier-shaped language name that is still unsafe for the generated enum literal—e.g. a Zig keyword such as error, or a name long enough to exceed the 96-byte scripting_lang_buf used by generateBuildZig—this predicate accepts it, validateDeclaredLanguage admits the row, and generation later emits invalid build code or hits the catch unreachable while formatting , .language = .{s}. The previous closed vocabulary only contained short non-keyword names, so row-declared languages need equivalent constraints here or the build.zig emission needs quoting/dynamic allocation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in da1a42b. isLanguageIdentifier now rejects Zig keywords (error/fn/test/…) — the generated .language = .<name> enum literal would be a syntax error otherwise. Enforced at manifest load (pointed PluginManifestInvalidLanguageRow) and in resolveProjectLanguage/requires_language. Primitives (i32) stay admissible since .i32 is a legal enum literal. Tests: keyword-named row rejected + isLanguageIdentifier keyword cases.

Comment thread src/language_policy.zig Outdated
Comment on lines +525 to +528
if (vocab.rowFor(lang) != null) continue;
try langs.append(allocator, .{
.name = lang,
.extensions = scriptExtensions(lang),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve built-in TypeScript metadata when rows shadow

When a migrated scripting manifest includes a row for a built-in language, this skips the frozen entry and later re-adds it with only the row's extensions and dir = r.name. For TypeScript rows, which typically declare authored .ts and .transpile.emits = "js", that drops the frozen .js policy extension and the ts/ legacy/misplaced-dir handling; e.g. a TypeScript project with files under typescript/ now passes even though resolveScriptDir still reads only scripts/ or ts/, and a Lua project stops flagging stray scripts/foo.js as foreign. Keep the frozen legacy dir/emitted-extension metadata when a row shadows a built-in.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in da1a42b. Vocabulary.build now UNIONS the frozen scriptExtensions into a shadowing row (row order first, deduped), so a migrated typescript row declaring only authored .ts keeps the frozen emitted .js; scanUnitLanguageDirs keeps the built-in legacy-dir mapping for shadowed languages instead of dropping it. (The full splice/declare/transpile metadata — TSC_PLATFORMS, staging geometry — is read directly from the manifest LanguageRow by the splice, not through this policy-side projection, so it was never at risk; this fixes the extension-set loss the scans depend on.) Test: Vocabulary.build shadow-row unions frozen extensions.

Comment thread src/language_policy.zig Outdated
try std.fmt.allocPrint(allocator, ".{s}", .{ext});
ext_len += 1;
}
try rows.append(allocator, .{ .name = name, .extensions = exts });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject rows without source extensions

When a new-language .languages row omits .extensions (the manifest type defaults it to empty), this still appends the row to the vocabulary, so validateDeclaredLanguage accepts .params.language even though scripting_splice.spliceFromRow later returns null for row.extensions.len == 0. That means a project can generate with the language declared but no scripting splice installed, and the policy scan has no extension to catch the corresponding scripts/ files, so the scripts are silently ignored instead of failing at manifest/policy validation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in da1a42b. A .languages row with empty .extensions is now rejected at manifest load with a pointed diagnostic (PluginManifestInvalidLanguageRow) — the shared-dir and plugin-event consumption scans find a language's sources by extension, so an extensionless row would silently elide every source it declares. Test: extensionless row rejected at load.

apotema added a commit that referenced this pull request Jul 19, 2026
…in metadata on shadow (codex #643 P2)

Three review fixes on the capability-row vocabulary:

1. Reject language names that break build.zig emission. isLanguageIdentifier
   now also rejects Zig keywords (`error`/`fn`/`test`/…): the generated
   `.language = .<name>` enum literal would be a syntax error otherwise.
   Enforced at manifest load (pointed PluginManifestInvalidLanguageRow) and
   in resolveProjectLanguage/requires_language. (Primitives like i32 stay
   admissible — `.i32` is a legal enum literal; only keywords break syntax.)

2. Preserve built-in metadata when a row shadows a built-in. Vocabulary.build
   now UNIONS the frozen scriptExtensions into a shadowing row's extensions
   (row order first, deduped), so a migrated typescript row declaring only
   authored `.ts` keeps the frozen emitted `.js`. scanUnitLanguageDirs keeps
   the built-in's legacy-dir mapping for shadowed languages instead of
   dropping it. (Splice/declare/transpile metadata — TSC_PLATFORMS, staging
   geometry — is read directly from the manifest LanguageRow, unaffected.)

3. Reject rows without source extensions. A `.languages` row with empty
   `.extensions` is rejected at manifest load with a pointed diagnostic —
   the shared-dir/consumption scans find sources by extension, so an
   extensionless row would silently elide every source it declares.

Tests: keyword-named row rejected, extensionless row rejected, well-formed
row loads, shadow-row unions frozen extensions, keyword identifiers rejected.
Baseline green (flow_catalog env lanes tolerated).

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
@apotema
apotema force-pushed the feat/619-language-rows branch from da1a42b to cb73c86 Compare July 19, 2026 18:34
apotema added a commit that referenced this pull request Jul 19, 2026
…in metadata on shadow (codex #643 P2)

Three review fixes on the capability-row vocabulary:

1. Reject language names that break build.zig emission. isLanguageIdentifier
   now also rejects Zig keywords (`error`/`fn`/`test`/…): the generated
   `.language = .<name>` enum literal would be a syntax error otherwise.
   Enforced at manifest load (pointed PluginManifestInvalidLanguageRow) and
   in resolveProjectLanguage/requires_language. (Primitives like i32 stay
   admissible — `.i32` is a legal enum literal; only keywords break syntax.)

2. Preserve built-in metadata when a row shadows a built-in. Vocabulary.build
   now UNIONS the frozen scriptExtensions into a shadowing row's extensions
   (row order first, deduped), so a migrated typescript row declaring only
   authored `.ts` keeps the frozen emitted `.js`. scanUnitLanguageDirs keeps
   the built-in's legacy-dir mapping for shadowed languages instead of
   dropping it. (Splice/declare/transpile metadata — TSC_PLATFORMS, staging
   geometry — is read directly from the manifest LanguageRow, unaffected.)

3. Reject rows without source extensions. A `.languages` row with empty
   `.extensions` is rejected at manifest load with a pointed diagnostic —
   the shared-dir/consumption scans find sources by extension, so an
   extensionless row would silently elide every source it declares.

Tests: keyword-named row rejected, extensionless row rejected, well-formed
row loads, shadow-row unions frozen extensions, keyword identifiers rejected.
Baseline green (flow_catalog env lanes tolerated).

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb73c86c54

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +916 to +917
// that both spells a valid enum literal and cannot overflow the
// fixed buffer. Dev-mode hot reload (#637): a

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Bound row language names before formatting

Fresh evidence after the prior fix: isLanguageIdentifier rejects keywords but still has no length cap, while generateBuildZig formats the value into a [96]u8 buffer with catch unreachable. A row-declared language name longer than 81 bytes (or longer than 47 bytes when hot reload is appended) passes manifest/policy validation and then panics during generation instead of producing a diagnostic; the old closed table only had short names. Please cap/reject names or switch this formatting to allocation.

Useful? React with 👍 / 👎.

Comment thread src/language_policy.zig
Comment on lines +246 to +253
for (in.extensions) |ext| {
const dotted = if (ext.len > 0 and ext[0] == '.')
try allocator.dupe(u8, ext)
else
try std.fmt.allocPrint(allocator, ".{s}", .{ext});
errdefer allocator.free(dotted);
try exts.append(allocator, dotted);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Do not admit extensions the splice never consumes

When a row declares multiple source extensions, e.g. .extensions = .{ ".py", ".pyw" }, this loop adds all of them to the project vocabulary, so scanUnitLanguageDirs treats scripts/foo.pyw as valid for the selected language. The actual splice still derives authored = row.extensions[0] in scripting_splice.spliceFromRow, and the consumption scan only receives s.extension/the transpile source, so non-first extensions are neither registered/embedded nor scanned for plugin-event references. Either consume/thread every declared extension or reject rows with more than one extension to avoid silently dead scripts.

Useful? React with 👍 / 👎.

// 2. `.extensions` — non-empty: the shared-dir + plugin-event
// consumption scans find a language's sources BY EXTENSION, so an
// extensionless row would silently elide every source it declares.
for (parsed.languages) |row| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject incomplete native rows before admitting them

A new .languages row such as .kind = .native with .extensions but without .module_root or .stage_subdir passes this manifest validation and is then admitted by validateDeclaredLanguage. The selected row cannot actually drive generation: scripting_splice.spliceFromRow returns null when either native field is absent, and for a non-built-in language detect falls through to no splice, so declared scripts/*.zc files are skipped by policy but never staged or wired. Please make native rows for non-fallback languages require both fields, or fail the selected-row detection instead of degrading to no integration.

Useful? React with 👍 / 👎.

Comment thread src/language_policy.zig
Comment on lines 781 to 785
if (declared) |d| {
if (std.mem.eql(u8, d.language, lang)) continue;
}
const matches = for (scriptExtensions(lang)) |ext| {
const matches = for (lang_entry.extensions) |ext| {
if (std.mem.endsWith(u8, entry.name, ext)) break true;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject extension collisions across row languages

If the selected row reuses an extension owned by another language, the shared-dir scan skips only the declared language and then continues matching the same filename against the remaining entries. For example, a new quokka row using .rb (or two rows both using .qk) makes scripts/behavior.rb/.qk in a quokka project get bucketed as a foreign language and fail before the row-driven splice can consume it. The built-in table relies on disjoint extensions for attribution, so row extensions need the same collision/suffix-overlap validation.

Useful? React with 👍 / 👎.

Comment on lines +472 to +473
for (pmani.languages, inputs) |row, *in| {
in.* = .{ .name = row.name, .extensions = row.extensions };

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include transpile emits in the policy vocabulary

For a new non-built-in transpiled row such as .name = "coffee", .extensions = .{".coffee"}, .transpile = .{ .emits = "js", ... }, this projection gives the language policy only the authored extension. The splice intentionally uses the emitted extension as s.extension and probes/collects scripts/*.js, but scanUnitLanguageDirs will still classify those .js files as the built-in TypeScript language and fail them as foreign before the splice can consume them. Please union the row's emitted extension into the declared language's policy extensions, or reject emitted extensions that another language owns.

Useful? React with 👍 / 👎.

// consumption scans find a language's sources BY EXTENSION, so an
// extensionless row would silently elide every source it declares.
for (parsed.languages) |row| {
if (!language_policy.isLanguageIdentifier(row.name)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reserve convention directory names as languages

A row named scripts passes this validation, but it makes normal scripts/*.sc projects unusable: language_policy.legacyDir("scripts") returns the same scripts/ directory as the shared convention dir, so resolveScriptDir sees both the top-level scripts probe and the legacy probe populated and raises LegacyScriptDirConflict. Since open row names can now collide with assembler-owned convention directories, reject at least scripts here or disable legacy fallback for row-declared languages.

Useful? React with 👍 / 👎.

apotema added 3 commits July 19, 2026 16:39
Open the assembler's language VOCABULARY so a language plugin can add a
brand-new language (the RFC's "python") through a `plugin.labelle`
`.languages` capability row alone — zero assembler changes. This closes
the last per-language hardcode the row-driven declare/transpile/embed/
native machinery (already on main via #621#624) sat on top of: the
closed `SUPPORTED_LANGUAGES`/`scriptExtensions` table that the policy
gate, the shared-dir scan, the plugin-event consumption scan, and
`requires_language` validation all consulted, and which rejected any
language it had never heard of before the row machinery ever ran.

What moved (RFC-LANGUAGE-PLUGINS rev 16 §7 "language-agnostic assembler"):

- `language_policy.Vocabulary` — the project's OPEN language set: the
  frozen `SUPPORTED_LANGUAGES` built-ins ∪ the declaring scripting
  plugin's manifest `.languages` rows. Rows are PRIMARY (a row naming a
  frozen language shadows its extension set); a row language the tables
  never heard of is admitted exactly like a built-in. `EMPTY` (no rows)
  reproduces the pre-#619 closed-vocabulary behavior bit for bit.
- `validateDeclaredLanguage` — the vocabulary gate, re-homed from
  `resolveProjectLanguage` into `generate_phases.validateLanguagePolicy`
  where the declaring plugin's manifest rows are in hand. Self-describing
  capabilities replace the closed table: a pointed error naming BOTH the
  built-ins and the manifest rows, no version compare.
- `resolveProjectLanguage` / manifest-load `requires_language` now gate
  on SHAPE only (`isLanguageIdentifier` — `[a-z][a-z0-9_]*`, the one
  constraint the generated `.language = .<name>` enum literal genuinely
  imposes); the vocabulary check runs at generate where rows are visible.
- `scanUnitLanguageDirs` / `checkRequiresLanguage` take the `Vocabulary`
  and police row languages by the row's extensions (frozen ∪ rows).
- The plugin-event consumption scan gains the active splice's script
  extensions (`filterConsumedEvents(extra_extensions)`) so a row
  language's sources (`.py`) are not invisible to the scan and its
  subscriptions silently elided — the comptime `scanned_extensions`
  covers only the frozen built-ins.

Migration map (hardcoded path → row-driven):
- closed `SUPPORTED_LANGUAGES` vocabulary check in `resolveProjectLanguage`
  → `Vocabulary.isKnown` (frozen ∪ manifest rows) in `validateDeclaredLanguage`
- `scriptExtensions`-only dir/event policing → `Vocabulary.extensionsOf`
  (row extensions primary, frozen fallback)
- manifest-load closed-table `requires_language` reject → shape-only reject
  + generate-time vocabulary check
- (already on main, unchanged) declare → `runGenericDeclarePhase` rows;
  embed/native/transpile → `spliceFromRow`; the frozen `DECLARE_RUNNERS`
  / `EMBED_LANGUAGES` / `NATIVE_LANGUAGES` / `TSC_PLATFORMS` tables stay
  FROZEN FALLBACKS for row-less (pre-migration) manifests.

Back-compat proof: `LUA_ROW_BACKCOMPAT` generates a lua project twice —
frozen fallback (no row) vs an equivalent `.languages` row — and asserts
the main.zig + build.zig are BYTE-IDENTICAL. Migrating lua/ruby onto rows
shifts nothing. Every existing golden/e2e suite is unchanged.

Litmus (`test/quokka_litmus_tests.zig`): a fake embedded-VM language
"quokka" (`.qk`) the assembler learns ENTIRELY from a manifest row
generates end-to-end (embed registration, `.language = .quokka` dep arg,
the four splice touchpoints), a foreign frozen-language file (`.rb`) in
the same project still errors (combined vocabulary polices both ways),
and a META test proves "quokka"/".qk" appear in NO `src/**/*.zig` — the
mechanical proof the generate rode rows, not a hidden branch.

Closes #619

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
…in metadata on shadow (codex #643 P2)

Three review fixes on the capability-row vocabulary:

1. Reject language names that break build.zig emission. isLanguageIdentifier
   now also rejects Zig keywords (`error`/`fn`/`test`/…): the generated
   `.language = .<name>` enum literal would be a syntax error otherwise.
   Enforced at manifest load (pointed PluginManifestInvalidLanguageRow) and
   in resolveProjectLanguage/requires_language. (Primitives like i32 stay
   admissible — `.i32` is a legal enum literal; only keywords break syntax.)

2. Preserve built-in metadata when a row shadows a built-in. Vocabulary.build
   now UNIONS the frozen scriptExtensions into a shadowing row's extensions
   (row order first, deduped), so a migrated typescript row declaring only
   authored `.ts` keeps the frozen emitted `.js`. scanUnitLanguageDirs keeps
   the built-in's legacy-dir mapping for shadowed languages instead of
   dropping it. (Splice/declare/transpile metadata — TSC_PLATFORMS, staging
   geometry — is read directly from the manifest LanguageRow, unaffected.)

3. Reject rows without source extensions. A `.languages` row with empty
   `.extensions` is rejected at manifest load with a pointed diagnostic —
   the shared-dir/consumption scans find sources by extension, so an
   extensionless row would silently elide every source it declares.

Tests: keyword-named row rejected, extensionless row rejected, well-formed
row loads, shadow-row unions frozen extensions, keyword identifiers rejected.
Baseline green (flow_catalog env lanes tolerated).

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
…cisions

Migrate the two `language == "csharp"` DECISIONS #644 added into
capability rows, per #619's design (the module doc #644 shipped explicitly
anticipated this):

- New LanguageRow capabilities `runtime_output` (the link-less build
  outputs ARE the runtime payload — stage beside the binary + set the run
  step's assembly-dir env) and `probe_tools` (opt into the generate-time
  PATH probe of the language's steps).
- scripting_csharp.stagesRuntimeOutputs now takes the resolved
  `runtime_output` capability instead of hardcoding `== "csharp"`; the
  ensureStepToolsOnPath call is gated on the resolved `probe_tools`.
- The csharp knowledge is DEMOTED to a frozen fallback
  (scripting_csharp.frozenRuntimeOutput/frozenProbeTools), the same
  #619 pattern as DECLARE_RUNNERS/EMBED_LANGUAGES/NATIVE_LANGUAGES — the
  shipped csharp manifest (which predates these capabilities) keeps working
  byte-for-byte; a NEW runtime-loaded language rides its row alone.
- ScriptingSplice.detect resolves both capabilities (row PRIMARY ∪ frozen)
  and threads them onto the splice; root.zig consumes them.

csharp's EMBED compile wiring itself needed NO migration — it was already
fully row-driven (csharp has no frozen-table fallback; it resolves entirely
via its manifest .languages native row + plugin-declared .language_builds).
Only the runtime-output-staging + toolchain-probe seams were csharp-named.

Residue (documented, RFC honest boundary): the runtime assembly-dir env var
NAME (LABELLE_CS_ASSEMBLY_DIR) stays an assembler-owned constant, and the
dev-.csproj IDE aid stays csharp-keyed MSBuild codegen — analogous to the
assembler-owned tsconfig codegen; neither is needed for the language-agnostic
litmus (which adds an EMBEDDED language).

Tests: runtime_output/probe_tools row parse (+ default-false), frozen
fallback (csharp opts in, others out), the capability-driven
stagesRuntimeOutputs. csharp_splice_tests (#644's e2e) stay green via the
frozen fallback — the back-compat proof. Baseline green (flow_catalog lanes).

Claude-Session: https://claude.ai/code/session_011szWvquoss1yNX7KWSKCaM
@apotema
apotema force-pushed the feat/619-language-rows branch from cb73c86 to 9149be1 Compare July 19, 2026 19:52
@apotema
apotema merged commit 89a78ce into main Jul 19, 2026
5 checks passed

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9149be10bd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

// vocabulary — bit-identical to the pre-#619 closed-table behavior.
var vocab = language_policy.Vocabulary.EMPTY;
defer vocab.deinit();
if (declared) |d| {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load row vocabulary when no language is declared

When a row-only scripting plugin is attached but the project forgets .params.language, this branch leaves vocab empty, so the later scanUnitLanguageDirs(..., declared = null, &vocab) only knows the frozen extensions. A project with scripts/behavior.qk and a plugin manifest declaring the quokka row then generates without the MissingScriptingPlugin error that a built-in .lua file would get, silently ignoring the script. Consider loading the attached scripting plugin's rows for the no-declaration scan, or otherwise failing row-language sources when no language is declared.

Useful? React with 👍 / 👎.

Comment thread src/language_policy.zig
Comment on lines +568 to +570
for (vocab.rows) |r| {
if (isSupportedLanguage(r.name)) continue; // already covered as a built-in above
try langs.append(allocator, .{ .name = r.name, .extensions = r.extensions, .dir = r.name });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject duplicate language rows

If a manifest repeats the same new language name, e.g. two quokka rows for .qk and .qq, this loop adds both entries to the scan vocabulary. walkSharedCollect skips every entry whose name matches the declared language, while PluginManifest.languageRow() and the splice use only the first row, so scripts/foo.qq passes policy but is never collected or embedded. Reject duplicate row names or merge them consistently before building the vocabulary.

Useful? React with 👍 / 👎.

Comment on lines +532 to +533
for (parsed.languages) |row| {
if (!language_policy.isLanguageIdentifier(row.name)) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject escaping native stage paths

When a native language row supplies an escaping .stage_subdir such as ../../.., this validation still admits the manifest. For a project with native sources and the required module root, stageNativeSources later joins that value under the staged plugin package and hands the result to scanner.linkDirAbs, which reconciles the destination with deleteTree before linking; an escaping row can therefore replace paths outside .labelle/.../deps/labelle-<plugin>. Validate native staging paths as safe relative subpaths before accepting the row.

Useful? React with 👍 / 👎.

Comment thread src/root.zig
Comment on lines +1547 to 1548
if (if (maybe_scripting) |s| s.probe_tools else false) {
try scripting_csharp.ensureStepToolsOnPath(allocator, plugin.name, dl, lang_steps_slice);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep probing splice-less csharp builds

When the selected language is csharp but no scripting splice is detected, this guard is now false even though the surrounding loop still loads and wires matching .language_builds for every cfg_modules plugin. For example, a non-scripting plugin declaring .params.language = "csharp" with a dotnet language build used to get the generate-time missing-tool diagnostic from the old dl == "csharp" check, but now the same missing SDK fails later as a child-spawn error during zig build. Keep the frozen csharp probe independent of maybe_scripting, or derive the probe capability from the selected language build entry.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant