Skip to content

Implement $fy: rule-based shell to mjs translation (JS + Rust)#133

Open
konard wants to merge 8 commits into
mainfrom
issue-1-7de9505b
Open

Implement $fy: rule-based shell to mjs translation (JS + Rust)#133
konard wants to merge 8 commits into
mainfrom
issue-1-7de9505b

Conversation

@konard

@konard konard commented Sep 9, 2025

Copy link
Copy Markdown
Member

Summary

Implements $fy (issue #1): a translator from shell scripts to command-stream
JavaScript modules — in both the JavaScript and the Rust implementation.

Following review feedback, the translation is genuinely rule-based on top of
link-foundation/meta-language
rather than string rewriting:

  1. Formalize — the script becomes a LinkNetwork of LinkType.Syntax nodes
    over LinkType.Token leaves, with a LanguageProfile declaring the supported
    surface (js/src/fy/shell-formalizer.mjs, rust/src/fy/formalizer.rs).
  2. Substitute — a TranslationRuleSet of ~40 TranslationRule /
    TranslationTemplate pairs, one per construct, selected per link by
    LinkQuery, rewrites that network into JavaScript
    (js/src/fy/translation-rules.mjs + rule-engine.mjs,
    rust/src/fy/rules.rs + engine.rs).

All shell and JavaScript knowledge lives in the rule table; the engine has none.

What it translates

Pipelines, && / || / ;, if / elif / else, while, until, for,
case, functions, local, export, redirects, set -e, comments, and
${NAME} / ${NAME:-default} / $1 / $@ / $# / $? / $$ /
$(...) / backtick expansions. Anything the rules cannot claim is reported as a
diagnostic instead of being silently mistranslated.

$fy deploy.sh              # print the translated module
$fy build.sh build.mjs     # write it to a file
echo "cd /tmp && ls" | $fy # translate a one-liner

JavaScript / Rust parity

fixtures/fy/sample.sh exercises every supported construct and
fixtures/fy/sample.mjs is the module both implementations produce from it,
byte for byte. rust/tests/fy.rs and js/tests/fy-tool.test.mjs each assert
against those files, so the two implementations cannot drift apart silently.

Missing meta-language features — reported

Ten gaps are documented with reproductions in
js/docs/meta-language-gaps.md
and filed upstream as link-foundation/meta-language#183. The blocking ones:
TranslationRuleSet.render substitutes no placeholders, does not recurse, and
applies only the first matching rule; and on the Rust side there is no public
dynamic-arity link insertion. rule-engine.mjs / engine.rs implement the
missing evaluation strategy over meta-language's own types and are written to
collapse into a single upstream call once those land.

Tests

  • js/tests/fy-tool.test.mjs — 30 tests: the formalizer's network shape, the
    rule set, the CLI, the shared fixture, and an end-to-end check that runs a
    translated script and compares its output with sh running the original.
  • rust/src/fy/* unit tests (38) plus rust/tests/fy.rs for the fixture.
  • js/examples/fy-translate.mjs and rust/examples/fy_translate.rs demonstrate
    both halves of the pipeline.

Fixes #1

Adding CLAUDE.md with task information for AI processing.
This file will be removed when the task is complete.

Issue: #1
@konard konard self-assigned this Sep 9, 2025
konard and others added 2 commits September 9, 2025 23:22
- Add $fy virtual command that converts shell scripts to command-stream mjs format
- Support stdin input for single commands and file input for full scripts
- Handle shell operators: &&, ||, ; with proper JavaScript equivalents
- Convert pipelines, sequences, variables, exports, and comments
- Preserve script structure while translating to command-stream syntax
- Add comprehensive test suite with 14 test cases
- Include example scripts and debug utilities

Features:
- Single command conversion: echo "ls -la" | $fy
- File conversion: $fy script.sh [output.mjs]
- Shell operator translation (&&, ||, ;)
- Pipeline preservation
- Comment and structure preservation
- Variable and export handling
- Error handling for missing files

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
@konard konard changed the title [WIP] tool (sh to mjs translator) tool (sh to mjs translator) - Issue #1 Sep 9, 2025
@konard
konard marked this pull request as ready for review September 9, 2025 20:30
@konard

konard commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

We should use https://github.com/link-foundation/meta-language, and may be logic from https://github.com/link-foundation/relative-meta-logic, in order to actually implement rule based translation, where we first formalize into meta language, and after it use substitution rules on top of meta language to actually finish the translation.

Any missing features from https://github.com/link-foundation/meta-language must be reported, so we can fix that first, before continuing this pull request.

Also may be you will learn something from github.com/link-assistant/formal-ai related to that specific task.

So we use all the best practices possible for both JavaScript and Rust.

@konard
konard marked this pull request as draft July 25, 2026 19:11
@konard

konard commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

🤖 AI Work Session Started

Starting automated work session at 2026-07-25T19:11:32.340Z

The PR has been converted to draft mode while work is in progress.

This comment marks the beginning of an AI work session. Please wait for the session to finish, and provide your feedback.

konard added 2 commits July 25, 2026 19:14
The first draft of $fy translated shell to JavaScript with ad-hoc string
munging. This replaces it with the two-stage pipeline requested in review:

1. `formalizeShell` parses the script and formalizes it into a
   link-foundation/meta-language `LinkNetwork` of typed syntax links
   (`LinkType.Syntax`, language `Shell`).
2. `buildRuleSet` declares Shell -> JavaScript `TranslationRule`s and a
   small engine substitutes them over the network, rendering each node in
   the sub-language its parent asks for (statement, command, value,
   expression).

Missing meta-language features found while doing this are reported in
docs/meta-language-gaps.md so they can be fixed upstream.

Also fixes two genuine bugs found on the way:

- `parseSimpleCommand` treated reserved words as block terminators in any
  position, so `echo done | tr a-z A-Z` lost its argument. Reserved words
  are only reserved in command position.
- `getStdinData` returned the stdio *mode* string as stdin *data*, so
  every virtual command saw the literal text `inherit` on stdin.

experiments/fy-pipeline-repro.sh reproduces the parser bug and
examples/fy-translate.mjs demonstrates the whole pipeline.
The reviewer asked for the best practices of both languages, so the
rule-based translator now exists in Rust as well: rust/src/fy mirrors
js/src/fy stage for stage (formalize into a meta-language links network,
then substitute through a TranslationRuleSet).

fixtures/fy/ holds one shell script and the module both implementations
must produce from it; rust/tests/fy.rs and js/tests/fy-tool.test.mjs
assert against those bytes, so the two cannot drift apart.

Also translate ${1:-default}: the brace expander only accepted names, so
positional and special parameters with a default fell through to the
untranslated \${...} escape hatch.

Reports two further meta-language findings: the Rust crate has no public
dynamic-arity link insertion (worked around by chunking), and
with_reference_capture exists in Rust 0.54 but not in JS 0.46.
@konard

konard commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

We should use meta-language, and maybe logic from relative-meta-logic, in order to actually implement rule based translation ... Any missing features from meta-language must be reported ... So we use all the best practices possible for both JavaScript and Rust.

Done. $fy is no longer a string rewriter; it is a two-stage rule-based translation, implemented twice — once in JavaScript, once in Rust — against meta-language's own types.

1. Formalize → substitute

  1. Formalize (js/src/fy/shell-formalizer.mjs, rust/src/fy/formalizer.rs): the shell source becomes a LinkNetwork of LinkType.Syntax nodes over LinkType.Token leaves, with a LanguageProfile declaring the supported surface (validateNetwork rejects anything outside it).
  2. Substitute (js/src/fy/translation-rules.mjs + rule-engine.mjs, rust/src/fy/rules.rs + engine.rs): a TranslationRuleSet of ~40 TranslationRule/TranslationTemplate pairs, one per construct, selected per link by LinkQuery. The rule table holds all the shell and JavaScript knowledge; the engine holds none.

2. Missing meta-language features — reported

Full write-up with reproductions in js/docs/meta-language-gaps.md, filed upstream as link-foundation/meta-language#183. Summary:

# Gap
1 TranslationRuleSet.render returns the template verbatim — placeholders are never substituted
2 No recursion into nested nodes
3 Only the first matching rule is ever applied (per network, not per link)
4 No reference-capture API on TranslationRule in JS (Rust 0.54 already has with_reference_capture)
5 No variadic placeholder (`{*children
6 No conditional template segments ({?name}…{/name})
7 No target sub-language / rendering context
8 No indentation-aware substitution
9 No builtin Shell profile; parse() emits no syntax nodes
10 Rust only: insert_syntax_node/insert_link are const-generic and insert_dynamic_link is pub(crate), so a run-time parser cannot insert a node of run-time arity

Gaps 1–3 block rule-based translation entirely, so rule-engine.mjs / engine.rs implement 1–8 over meta-language's own types and are written to collapse into a single upstream call once these land. Gap 10 is worked around by chunking wide nodes and flattening them again in the engine.

Also confirmed on the Rust side by reading the crate: TranslationRuleSet::render is pub(crate), applies only the first matching rule, and does not recurse.

3. JavaScript / Rust parity

fixtures/fy/sample.sh is a script exercising every supported construct; fixtures/fy/sample.mjs is the module both implementations produce from it, byte for byte. rust/tests/fy.rs and js/tests/fy-tool.test.mjs each assert against those files, so the two cannot drift apart silently.

cargo test is green (161 lib + all integration tests, 38 of them new fy tests); bun test tests/fy-tool.test.mjs is 30/30. The JS suite also executes a translated script and compares its output to sh running the original.

@konard konard changed the title tool (sh to mjs translator) - Issue #1 Implement $fy: rule-based shell to mjs translation (JS + Rust) Jul 25, 2026
konard added 2 commits July 25, 2026 20:39
The Node.js compatibility job imports js/src/$.mjs without installing
dependencies, and the $fy command pulled meta-language in statically, so
the module no longer loaded on Node:

  Module failed to load: Cannot find package 'meta-language' imported
  from js/src/fy/shell-formalizer.mjs

Importing the translator inside the command handler keeps meta-language
out of the library's import graph until $fy actually runs.
The Windows job failed on both new tests. A script checked out with CRLF
endings reached the lexer with the \r attached to each word, which broke
the parse ('rule `for` has no JavaScript:command template'), so both
translators now normalize line endings first. The golden fixture must
also arrive with LF for a byte-for-byte comparison to mean anything,
which .gitattributes now enforces.

The run-the-translation test passed a URL pathname as a module
specifier; on Windows that is '/D:/...', which is not importable, so it
passes the file URL instead.
@konard
konard marked this pull request as ready for review July 25, 2026 20:51
@konard

konard commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

🤖 Solution Draft Log

This log file contains the complete execution trace of the AI solution draft process.

💰 Cost: $29.524047

📊 Context and tokens usage:

Claude Opus 5: (7 sub-sessions)

  1. 112.5K / 1M (11%) input tokens, 41.6K / 128K (33%) output tokens
  2. 117.0K / 1M (12%) input tokens, 40.3K / 128K (31%) output tokens
  3. 116.9K / 1M (12%) input tokens, 23.6K / 128K (18%) output tokens
  4. 115.1K / 1M (12%) input tokens, 29.2K / 128K (23%) output tokens
  5. 116.3K / 1M (12%) input tokens, 33.9K / 128K (26%) output tokens
  6. 116.9K / 1M (12%) input tokens, 26.0K / 128K (20%) output tokens
  7. 37.0K / 1M (4%) input tokens, 867 / 128K (1%) output tokens

Total: (16.2K new + 593.9K cache writes + 34.2M cache reads) input tokens, 256.4K output tokens, $29.524047 cost

🤖 Models used:

  • Tool: Anthropic Claude Code
  • Requested: opus (claude-opus-5)
  • Thinking level: off (disabled)
  • Model: Claude Opus 5 (claude-opus-5)

📎 Log file uploaded as Gist (10705KB)


Now working session is ended, feel free to review and add any feedback on the solution draft.

@konard

konard commented Jul 25, 2026

Copy link
Copy Markdown
Member Author

✅ Ready to merge

This pull request is now ready to be merged:

  • All CI checks have passed
  • No merge conflicts
  • No pending changes

Monitored by hive-mind with --auto-restart-until-mergeable flag

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.

$fy tool (sh to mjs translator)

1 participant