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 (program, warnings) = check_single_file(source, SingleFileMode::Build)?;
55 Ok(Compiled {
56 ts: emitter::emit(&program),
57 warnings,
58 })
59}
60
61/// Check a single-file commons without building it: the same diagnostics as
62/// [`compile_with_warnings`], minus the build-only ones, and no emission.
63/// Returns the non-failing warnings on success. The single-file half of
64/// `bynk check` / `bynkc check` (`bynk_driver::run_check`); a directory goes
65/// through the project pipeline in `Mode::Check` instead.
66///
67/// #1883, slice 1a (review of #1903): before this, `run_check` reused
68/// [`compile_with_warnings`], so `bynk check page.bynk` failed with the
69/// build-only asset refusal, contradicting §5.12's "checking is unaffected".
70pub fn check_with_warnings(
71 source: &str,
72 _filename: &str,
73) -> Result<Vec<CompileError>, Vec<CompileError>> {
74 check_single_file(source, SingleFileMode::Check).map(|(_, warnings)| warnings)
75}
76
77/// Whether a single-file run will go on to emit (`Build`) or only report
78/// (`Check`). Only build-only diagnostics differ: today, the asset gate.
79#[derive(Clone, Copy, PartialEq, Eq)]
80enum SingleFileMode {
81 Check,
82 Build,
83}
84
85/// The shared single-file pipeline: lex, parse, check, certify.
86fn check_single_file(
87 source: &str,
88 mode: SingleFileMode,
89) -> Result<(checker::CheckedProgram, Vec<CompileError>), Vec<CompileError>> {
90 let tokens = lexer::tokenize(source).map_err(|e| vec![e])?;
91 // ADR 0117: parse-time warnings (orphan doc blocks) ride alongside the
92 // AST — they surface with the build's warnings instead of failing it.
93 let (commons, mut warnings) = match parser::parse_with_warnings(&tokens, source) {
94 Ok(parsed) => parsed,
95 // #1663: the strict parse stops at the first syntax error. Diagnose the
96 // file as the editor does instead, and still refuse to compile it.
97 Err(strict) => {
98 return Err(bynk_check::recovery::diagnose_unparsable(
99 &tokens, source, strict,
100 ));
101 }
102 };
103 // v0.20a: function types are confined to non-boundary positions — the same
104 // rule the project path applies.
105 let mut boundary_errors = Vec::new();
106 let boundary_types = bynk_check::project_model::collect_type_decls(commons.items.iter());
107 bynk_check::project_model::check_function_type_boundary_items(
108 &commons.items,
109 &boundary_types,
110 &mut boundary_errors,
111 );
112 // #1883: the asset declaration rules (path, duplicates, name conflicts),
113 // which the project path raises in `build_unit_table`; and, in a build
114 // only, `bynk.asset.needs_project` — a single file has no project root to
115 // find `assets/` under and emits no Worker to serve it beside.
116 boundary_errors.extend(bynk_check::assets::single_file_asset_errors(
117 &commons.items,
118 mode == SingleFileMode::Build,
119 ));
120 // #2387: the same for `embed` — its path and name rules, and, in
121 // a build only, `bynk.embed.needs_project` — a single file has no project root
122 // to find `embeds/` under.
123 boundary_errors.extend(bynk_check::embeds::single_file_embed_errors(
124 &commons.items,
125 mode == SingleFileMode::Build,
126 ));
127 // #1663: a boundary or resolve error is local to its declaration, so the
128 // checker still runs over every declaration and its diagnostics join
129 // theirs (minus echoes); any error still refuses the compile.
130 let item_spans: Vec<_> = commons.items.iter().map(|i| i.span()).collect();
131 let (resolved, resolve_errors) = resolver::resolve_recovering(commons);
132 let mut errors = boundary_errors;
133 errors.extend(resolve_errors.iter().cloned());
134 let typed = match checker::check(resolved) {
135 Ok(typed) if errors.is_empty() => typed,
136 Ok(_) => return Err(errors),
137 Err(checked) => {
138 errors.extend(resolver::without_resolve_echoes(
139 checked,
140 &resolve_errors,
141 &item_spans,
142 ));
143 return Err(errors);
144 }
145 };
146 warnings.extend(typed.warnings.clone());
147 // T3.7 (R3.10): `check` already gated on error-severity diagnostics, so
148 // `typed.warnings` — the only diagnostics left riding along with it — can
149 // never contain one; `certify` re-asserts that structurally rather than
150 // trusting the caller not to skip it.
151 let program = checker::certify(typed, warnings.clone()).unwrap_or_else(|_| {
152 panic!("bynk internal error: check() already gated on error-severity diagnostics")
153 });
154 Ok((program, warnings))
155}
156
157/// Minimum supported Node.js **major** version for the `node` platform binding
158/// and for running Bynk's emitted TypeScript.
159///
160/// Single source of truth for the Node floor: the emitted code targets it, the
161/// `bynk` driver's `doctor` command compares a detected `node` against it, and
162/// `bynkc`'s CLI re-exports it rather than restating the number. Lives in
163/// `bynk-emit` (which emits the TS that runs on Node) so both binaries share one
164/// definition (slice 7; was a `bynkc` const before the driver dropped that dep).
165///
166/// #1674: 22, the oldest Node major still in support (18 reached end-of-life in
167/// April 2025, 20 in April 2026), and the one `bynkc test --inspect` needs. So
168/// `bynk doctor`'s `ok` means the commands work, not merely that Node exists.
169/// The `--inspect` path checks its own minor floor (22.6 to run, 22.18 for
170/// source-mapped breakpoints) and says so when it isn't met.
171pub const NODE_MAJOR_FLOOR: u32 = 22;
172
173/// #1672: the oldest TypeScript major the emitted output is verified against.
174/// CI type-checks every positive fixture under it (`tsc_verify`, with
175/// `typescript@5` installed on every test leg).
176pub const TYPESCRIPT_MAJOR_FLOOR: u32 = 5;
177
178/// #1672: the newest TypeScript major the emitted output is verified against —
179/// the one users get from `npm install -g typescript`. CI type-checks every
180/// positive fixture under it too (a second `tsc_verify` pass on the Linux leg),
181/// the `npx` fallbacks provision it, and `bynk doctor` reports a `tsc` newer
182/// than it as untested.
183pub const TYPESCRIPT_MAJOR_TESTED: u32 = 7;
184
185/// I2 (#1971): the value of every edge key off the deployed path. `bynk dev`
186/// passes it for each edge a Worker is on (`BYNK_CALL_TO_<callee>`,
187/// `BYNK_CALL_FROM_<caller>`, named by `bynk_check::wire_format`), and the
188/// generated test harnesses put it in every participant's `env`. Fixed rather
189/// than random so two `bynk dev --context` sessions run side by side still
190/// admit each other's calls. It guards nothing, so it must never reach a
191/// deployed Worker: `bynk deploy` mints one key per edge, and none is ever
192/// written into a generated `wrangler.toml`.
193pub const LOCAL_CALL_KEY: &str = "bynk-local-call-key";
194
195/// State-migrations S4 (#2259): the call signature and the sweep route's
196/// names, for `bynk migrate sweep`, which signs as the runtime verifies.
197pub use bynk_check::wire_format;
198/// I2 (#1971): the edge keys' names, for the driver (`bynk dev`, `bynk
199/// deploy`), which sees the compiler only through this crate.
200pub use bynk_check::wire_format::{EDGE_FROM_PREFIX, EDGE_TO_PREFIX, edge_key_from, edge_key_to};
201
202pub use emitter::wrangler::{
203 COMPATIBILITY_DATE, DEFAULT_COMPATIBILITY_FLAGS, WRANGLER_MIN, compatibility_flags,
204};
205
206// `write_output`/`write_document` moved to `bynk-driver` (#1047, R2.3/
207// T0.7 residue): every caller was already at driver level, so this crate
208// never needed direct filesystem access for it — the pure move closes it
209// out of this crate's `fs_below_driver` count. See `bynk-driver::output`.