Skip to main content

bynk_emit/
lib.rs

1//! Bynk's TypeScript emission, plus the per-unit build sequencing that drives
2//! it — the layer above `bynk-project` (discovery, the dependency graph),
3//! `bynk-check` (all semantic checking, R3.5), and (since the P7.12 crate
4//! carve) `bynk-ir`/`bynk-lower` (the typed IR and its `&CheckedProgram → Ir`
5//! lowering pass).
6//!
7//! `project` owns `compile_project`/`run_checks`: the two-pass sequence over
8//! a project's units — discover and parse (`bynk-project`), then resolve,
9//! type-check (`bynk-check`) and emit each unit with full visibility of what
10//! it `uses`/`consumes`. `emitter` lowers a checked program to TypeScript.
11//! Input: a project tree (or an in-memory overlay). Output: TypeScript files
12//! plus diagnostics — this crate originates none of its own (P5.5,
13//! `design/tracks/semantics-in-the-checker.md` §3.5, R10.1).
14//!
15//! Extracted from `bynkc` as slice 4 of the crate-decomposition track over
16//! `bynk-syntax` + `bynk-check`. Behaviour is unchanged; `bynkc` depends on this
17//! crate and re-exports its modules so its public API (`compile_project`,
18//! `ProjectOutput`, …) and the binary are untouched.
19
20pub mod emitter;
21pub mod project;
22
23#[cfg(test)]
24pub(crate) mod testkit;
25
26use bynk_check::{checker, resolver};
27use bynk_syntax::{CompileError, lexer, parser};
28
29/// A single-file compile that also returns the non-failing warnings produced on
30/// success — what a CLI prints (v0.89, ADR 0117). [`compile`] is the
31/// warning-discarding convenience over this.
32///
33/// Lives in `bynk-emit` (slice 7 precedent, alongside [`NODE_MAJOR_FLOOR`]) so
34/// both `bynkc` and the `bynk` driver can compile a self-contained single-file
35/// commons in-process without depending on each other; `bynkc` re-exports it so
36/// `bynkc::compile_with_warnings` and `bynkc::Compiled` are unchanged.
37pub struct Compiled {
38    pub ts: String,
39    pub warnings: Vec<CompileError>,
40}
41
42/// Compile a single Bynk source string to a TypeScript string.
43///
44/// Parses the input as a self-contained, single-file commons with no `uses`
45/// against other commons. Use [`project::compile_project`] for multi-file
46/// projects or for any source that declares `uses`. `filename` is used only for
47/// diagnostic rendering.
48pub fn compile(source: &str, filename: &str) -> Result<String, Vec<CompileError>> {
49    compile_with_warnings(source, filename).map(|c| c.ts)
50}
51
52/// The warning-preserving single-file compile behind [`compile`]. See [`Compiled`].
53pub fn compile_with_warnings(source: &str, _filename: &str) -> Result<Compiled, Vec<CompileError>> {
54    let tokens = lexer::tokenize(source).map_err(|e| vec![e])?;
55    // ADR 0117: parse-time warnings (orphan doc blocks) ride alongside the
56    // AST — they surface with the build's warnings instead of failing it.
57    let (commons, mut warnings) = match parser::parse_with_warnings(&tokens, source) {
58        Ok(parsed) => parsed,
59        // #1663: the strict parse stops at the first syntax error. Diagnose the
60        // file as the editor does instead, and still refuse to compile it.
61        Err(strict) => {
62            return Err(bynk_check::recovery::diagnose_unparsable(
63                &tokens, source, strict,
64            ));
65        }
66    };
67    // v0.20a: function types are confined to non-boundary positions — the same
68    // rule the project path applies.
69    let mut boundary_errors = Vec::new();
70    let boundary_types = bynk_check::project_model::collect_type_decls(commons.items.iter());
71    bynk_check::project_model::check_function_type_boundary_items(
72        &commons.items,
73        &boundary_types,
74        &mut boundary_errors,
75    );
76    // #1663: a boundary or resolve error is local to its declaration, so the
77    // checker still runs over every declaration and its diagnostics join
78    // theirs (minus echoes); any error still refuses the compile.
79    let item_spans: Vec<_> = commons.items.iter().map(|i| i.span()).collect();
80    let (resolved, resolve_errors) = resolver::resolve_recovering(commons);
81    let mut errors = boundary_errors;
82    errors.extend(resolve_errors.iter().cloned());
83    let typed = match checker::check(resolved) {
84        Ok(typed) if errors.is_empty() => typed,
85        Ok(_) => return Err(errors),
86        Err(checked) => {
87            errors.extend(resolver::without_resolve_echoes(
88                checked,
89                &resolve_errors,
90                &item_spans,
91            ));
92            return Err(errors);
93        }
94    };
95    warnings.extend(typed.warnings.clone());
96    // T3.7 (R3.10): `check` already gated on error-severity diagnostics, so
97    // `typed.warnings` — the only diagnostics left riding along with it — can
98    // never contain one; `certify` re-asserts that structurally rather than
99    // trusting the caller not to skip it.
100    let program = checker::certify(typed, warnings.clone()).unwrap_or_else(|_| {
101        panic!("bynk internal error: check() already gated on error-severity diagnostics")
102    });
103    Ok(Compiled {
104        ts: emitter::emit(&program),
105        warnings,
106    })
107}
108
109/// Minimum supported Node.js **major** version for the `node` platform binding
110/// and for running Bynk's emitted TypeScript.
111///
112/// Single source of truth for the Node floor: the emitted code targets it, the
113/// `bynk` driver's `doctor` command compares a detected `node` against it, and
114/// `bynkc`'s CLI re-exports it rather than restating the number. Lives in
115/// `bynk-emit` (which emits the TS that runs on Node) so both binaries share one
116/// definition (slice 7; was a `bynkc` const before the driver dropped that dep).
117///
118/// #1674: 22, the oldest Node major still in support (18 reached end-of-life in
119/// April 2025, 20 in April 2026), and the one `bynkc test --inspect` needs. So
120/// `bynk doctor`'s `ok` means the commands work, not merely that Node exists.
121/// The `--inspect` path checks its own minor floor (22.6 to run, 22.18 for
122/// source-mapped breakpoints) and says so when it isn't met.
123pub const NODE_MAJOR_FLOOR: u32 = 22;
124
125/// #1672: the oldest TypeScript major the emitted output is verified against.
126/// CI type-checks every positive fixture under it (`tsc_verify`, with
127/// `typescript@5` installed on every test leg).
128pub const TYPESCRIPT_MAJOR_FLOOR: u32 = 5;
129
130/// #1672: the newest TypeScript major the emitted output is verified against —
131/// the one users get from `npm install -g typescript`. CI type-checks every
132/// positive fixture under it too (a second `tsc_verify` pass on the Linux leg),
133/// the `npx` fallbacks provision it, and `bynk doctor` reports a `tsc` newer
134/// than it as untested.
135pub const TYPESCRIPT_MAJOR_TESTED: u32 = 7;
136
137pub use emitter::wrangler::{COMPATIBILITY_DATE, WRANGLER_MIN};
138
139// `write_output`/`write_document` moved to `bynk-driver` (#1047, R2.3/
140// T0.7 residue): every caller was already at driver level, so this crate
141// never needed direct filesystem access for it — the pure move closes it
142// out of this crate's `fs_below_driver` count. See `bynk-driver::output`.