diff --git a/bynk-emit/src/emitter.rs b/bynk-emit/src/emitter.rs index c0204bd5b..9542cc291 100644 --- a/bynk-emit/src/emitter.rs +++ b/bynk-emit/src/emitter.rs @@ -23,7 +23,10 @@ use std::sync::Arc; use self::source_map::SourceMapBuilder; -use crate::ir::lower::{lower_capability_item_ir, lower_type_item_ir}; +use crate::ir::lower::{ + lower_capability_item_ir, lower_protocol_ir, lower_service_handler_signature_ir, + lower_type_item_ir, +}; use crate::ir::{IrItem, TypeShape}; use crate::project::{BuildTarget, EmitProjectCtx, ImportExt, UnitKind}; use bynk_check::builtin_names::map_query; @@ -411,7 +414,30 @@ pub(crate) fn emit_project( } CommonsItem::Service(s) => { smb.borrow_mut().record(out.len(), s.span); - emit_service(&mut out, s, commons, ctx, Some(&smb)); + // #1187's slice 5: `emit_service` reads the protocol's own + // resolved data (`ProtocolIr`) and each handler's resolved + // signature (params/ret/effectful) instead of `s`'s own raw + // `ServiceProtocol`/`TypeRef`s — not a full `IrItem::Service` + // (see `lower_service_handler_signature_ir`'s own doc + // comment for why: a real `IrHandler` would unconditionally + // lower every handler's body, panicking on an ordinary + // `Ok`/`Err`-returning Http handler). No separate helper, + // this is `emit_service`'s one and only call site. + let protocol = lower_protocol_ir(&s.protocol, program); + let signatures: Vec<_> = s + .handlers + .iter() + .map(|h| lower_service_handler_signature_ir(h, program)) + .collect(); + emit_service( + &mut out, + s, + &protocol, + &signatures, + commons, + ctx, + Some(&smb), + ); } CommonsItem::Agent(a) => { smb.borrow_mut().record(out.len(), a.span); diff --git a/bynk-emit/src/emitter/emit.rs b/bynk-emit/src/emitter/emit.rs index 7255d8828..816738618 100644 --- a/bynk-emit/src/emitter/emit.rs +++ b/bynk-emit/src/emitter/emit.rs @@ -14,8 +14,8 @@ use std::sync::Arc; use crate::project::EmitProjectCtx; use bynk_check::checker::{TypedCommons, Types}; -use crate::ir::lower::body_writes_state; -use crate::ir::{OpSig, TypeShape}; +use crate::ir::lower::{HandlerSignatureIr, body_writes_state}; +use crate::ir::{OpSig, ProtocolIr, TypeShape}; use super::*; @@ -1274,16 +1274,25 @@ pub(crate) fn emit_provider( pub(crate) fn emit_service( out: &mut String, s: &ServiceDecl, + protocol: &ProtocolIr, + signatures: &[HandlerSignatureIr], commons: &TypedCommons, ctx: &EmitProjectCtx, source_map: Option<&RefCell>, ) { + let tys = commons.tys(); emit_doc_block(out, s.documentation.as_deref(), 0); writeln!(out, "export const {name} = {{", name = s.name.name).unwrap(); let mut cron_idx = 0usize; let mut queue_idx = 0usize; - let ws_proto = matches!(s.protocol, ServiceProtocol::WebSocket { .. }); - for handler in &s.handlers { + let ws_proto = matches!(protocol, ProtocolIr::WebSocket { .. }); + // #1187's slice 5: `s.handlers`/`signatures` are the same list in the + // same declaration order — `signatures` is built by mapping `s.handlers` + // 1:1 at the one call site (`emitter.rs`) — the same zip-by-index + // precedent `emit_capability`'s own `c.ops`/`ops` pairing already + // established (#1193). + for (handler, (ir_params, _ir_given, ir_ret, ir_effectful)) in s.handlers.iter().zip(signatures) + { // v0.104/v0.106 (real-time track slice 3b): on Workers a `from websocket` // lifecycle handler (`on open`/`on message`/`on close`) does not emit a // service-surface method — its body runs inside the hosting Durable Object @@ -1330,19 +1339,18 @@ pub(crate) fn emit_service( // For service handlers the operation name is the handler kind // (e.g. `call`). v0.5 has only one handler kind, so the service is a // single-operation object literal. - let mut params: Vec = handler - .params + let mut params: Vec = ir_params .iter() - .map(|p| format!("{}: {}", ts_ident(&p.name.name), ts_type_ref(&p.type_ref))) + .map(|(name, ty)| format!("{}: {}", ts_ident(name), ts_ty(*ty, tys))) .collect(); // v0.103/v0.106: a `from websocket` lifecycle handler receives the // `connection` as its first parameter (the synthetic binding the checker // added — the fresh socket for `on open`, the firing socket for `on // message`/`on close`); emit it so the lowered body's `connection` resolves. - if is_ws_handler && let ServiceProtocol::WebSocket { out_type, .. } = &s.protocol { + if is_ws_handler && let ProtocolIr::WebSocket { out_ty, .. } = protocol { params.insert( 0, - format!("connection: Connection<{}>", ts_type_ref(out_type)), + format!("connection: Connection<{}>", ts_ty(*out_ty, tys)), ); } // Events track, slice 4 (spine #936): a `via schema(N)` guard needs @@ -1355,10 +1363,10 @@ pub(crate) fn emit_service( // envelope-forwarding call sites (`workers.rs`, `project.rs`) widen // their own condition to match, so the value actually arrives here. let schema_dispatch_env_binder = if handler.kind == HandlerKind::Event - && let ServiceProtocol::Events { + && let ProtocolIr::Events { schema_dispatch: Some(_), .. - } = &s.protocol + } = protocol { match handler.params.get(1) { Some(env_param) => Some(ts_ident(&env_param.name.name)), @@ -1452,7 +1460,7 @@ pub(crate) fn emit_service( ) .with_source_map(Some(&body_smb)); cx.local_agents = ctx.local_agents.clone(); - let async_tail = is_effectful_return(&handler.return_type); + let async_tail = *ir_effectful; emit_block_as_function_body_with_return( &mut body_out, &handler.body, @@ -1473,12 +1481,12 @@ pub(crate) fn emit_service( // event type by `check_service_protocols`'s param-type-agreement // check, so testing its fields here is sound. if handler.kind == HandlerKind::Event - && let ServiceProtocol::Events { + && let ProtocolIr::Events { pattern: Some(pattern), .. - } = &s.protocol + } = protocol && let Some(param) = handler.params.first() - && let Some(guard) = event_pattern_guard(&ts_ident(¶m.name.name), Some(pattern)) + && let Some(guard) = event_pattern_guard_ir(&ts_ident(¶m.name.name), Some(pattern)) { let prologue = format!( "{}if (!({guard})) return undefined;\n", @@ -1492,13 +1500,13 @@ pub(crate) fn emit_service( // prologue technique, same three-delivery-path coverage. The // envelope binder is either the user's own declared `env` name or // the synthetic one inserted above. - if let ServiceProtocol::Events { + if let ProtocolIr::Events { schema_dispatch: Some(dispatch), .. - } = &s.protocol + } = protocol && let Some(env_binder) = &schema_dispatch_env_binder { - let SchemaVersionPattern::Literal(version) = &dispatch.pattern; + let SchemaVersionPattern::Literal(version) = dispatch; let prologue = format!( "{}if (!({env_binder}.schemaVersion === {version})) return undefined;\n", " ".repeat(INDENT_STEP * 2) @@ -1624,12 +1632,8 @@ pub(crate) fn emit_service( }; } params.push(format!("deps: {deps_ty}")); - let ret = ts_type_ref(&handler.return_type); - let async_kw = if is_effectful_return(&handler.return_type) { - "async " - } else { - "" - }; + let ret = ts_ty(*ir_ret, tys); + let async_kw = if *ir_effectful { "async " } else { "" }; writeln!( out, " {async_kw}{op}({params}): {ret} {{", diff --git a/bynk-emit/src/emitter/lower.rs b/bynk-emit/src/emitter/lower.rs index e64f01138..4567e6fcc 100644 --- a/bynk-emit/src/emitter/lower.rs +++ b/bynk-emit/src/emitter/lower.rs @@ -8,6 +8,8 @@ use std::sync::Arc; use bynk_check::checker::{NamedKind, Ty, TyId, TypedCommons}; +use crate::ir::{ConstVal, EventPatternIr, EventPatternValueIr}; + use super::*; /// Lower a block to a sequence of TypeScript statements suitable for use as @@ -5379,41 +5381,98 @@ fn pattern_match_tests( } } -/// Events track, slice 1 (spine #936): the JS boolean guard for a -/// `from Events(E { field: value, .. })` subscription filter, AND-joining one -/// test per listed field. Parallel in shape to [`pattern_match_tests`] but -/// **not** a call into it — that function's `Refined` arm panics on a -/// non-literal-kind scrutinee and its `Variant` arm tests `.tag` on the -/// scrutinee itself, neither of which fits a flat record-field test against -/// an [`EventPattern`], which is a distinct AST node precisely because an -/// event is a plain record, not a sum (see `EventPattern`'s doc comment, -/// which also records the ADR 0286 amendment this represents). No `Ty`/ -/// `LowerCtx` needed: `check_event_pattern` -/// (`bynk-emit/src/project/validate.rs`) has already proven every field -/// exists and every value type-checks, so this is total and cannot panic. -/// Returns `None` for a pattern-less subscription (nothing to guard). -pub(crate) fn event_pattern_guard(path: &str, pattern: Option<&EventPattern>) -> Option { +/// #1187's slice 5 (the `Service` emitter cutover): the JS boolean guard for +/// a `from Events(E { field: value, .. })` subscription filter, AND-joining +/// one test per listed field — reads [`crate::ir::EventPatternIr`] (already +/// resolved by [`crate::ir::lower::lower_protocol_ir`]) rather than the raw +/// AST `EventPattern`; [`EventPatternValueIr`]'s own doc comment already +/// named this function as its sole intended consumer, never wired up until +/// now (the AST-driven original this replaces produced byte-identical guard +/// text: `EventPatternValueIr::Const` only ever holds the same closed +/// `Int`/`Str`/`Bool` set `literal_case_label` covers, and `Variant { tag }` +/// already carries exactly the bare, unqualified tag the AST version's +/// `variant.name` destructured down to). No `Ty`/`LowerCtx` needed: +/// `check_event_pattern` (`bynk-check/src/project_model.rs`) has already +/// proven every field exists and every value type-checks, so this is total +/// and cannot panic. Returns `None` for a pattern-less subscription (nothing +/// to guard). +pub(crate) fn event_pattern_guard_ir( + path: &str, + pattern: Option<&EventPatternIr>, +) -> Option { let pattern = pattern?; let tests: Vec = pattern .fields .iter() - .map(|f| match &f.value { - // `payload as any` on the wire (Workers `workers_entry.rs`) or a - // real object (Bundle) either way survives a JSON round-trip - // unchanged, and a nullary variant constructs as `{ tag: "..." }` - // (`emit.rs`'s sum-constructor emission) — so `.tag` is the - // correct test on both targets. - EventPatternValue::Literal { value, .. } => { - format!("{path}.{} === {}", f.name.name, literal_case_label(value)) + .map(|(name, value)| match value { + EventPatternValueIr::Const(ConstVal::Int(n)) => format!("{path}.{name} === {n}"), + EventPatternValueIr::Const(ConstVal::Str(s)) => { + format!("{path}.{name} === \"{}\"", escape_ts_string(s)) } - EventPatternValue::Variant { variant, .. } => { - format!("{path}.{}.tag === \"{}\"", f.name.name, variant.name) + EventPatternValueIr::Const(ConstVal::Bool(b)) => format!("{path}.{name} === {b}"), + EventPatternValueIr::Const( + ConstVal::Float(_) | ConstVal::DurationMillis(_) | ConstVal::Unit, + ) => { + unreachable!( + "EventPatternValueIr::Const only ever holds Int/Str/Bool (its own doc \ + comment) — the checker's own check_event_pattern already restricted a \ + from Events(...) filter to that closed set before this pattern could exist" + ) } + EventPatternValueIr::Variant { tag } => format!("{path}.{name}.tag === \"{tag}\""), }) .collect(); Some(tests.join(" && ")) } +#[cfg(test)] +mod event_pattern_guard_ir_tests { + use super::*; + + /// Review of #1198: the byte-identity evidence against the deleted + /// AST-driven `event_pattern_guard` was entirely fixture-level. This + /// pins the guard text format directly, over a hand-built + /// `EventPatternIr` covering all four cases `EventPatternValueIr` can + /// hold (`Int`/`Str`/`Bool`/`Variant`). + #[test] + fn covers_every_value_shape() { + let pattern = EventPatternIr { + fields: vec![ + ( + "n".to_string(), + EventPatternValueIr::Const(ConstVal::Int(3)), + ), + ( + "s".to_string(), + EventPatternValueIr::Const(ConstVal::Str("a\"b".to_string())), + ), + ( + "b".to_string(), + EventPatternValueIr::Const(ConstVal::Bool(true)), + ), + ( + "status".to_string(), + EventPatternValueIr::Variant { + tag: "Active".to_string(), + }, + ), + ], + }; + assert_eq!( + event_pattern_guard_ir("e", Some(&pattern)), + Some( + "e.n === 3 && e.s === \"a\\\"b\" && e.b === true && e.status.tag === \"Active\"" + .to_string() + ) + ); + assert_eq!( + event_pattern_guard_ir("e", None), + None, + "a pattern-less subscription has nothing to guard" + ); + } +} + /// Emit `const` declarations binding the names in `pattern` from runtime `path`, /// recursing through nested payloads (ADR 0169). fn emit_pattern_bindings( diff --git a/bynk-emit/src/ir.rs b/bynk-emit/src/ir.rs index fde6f7e55..4b7e62fcf 100644 --- a/bynk-emit/src/ir.rs +++ b/bynk-emit/src/ir.rs @@ -1039,11 +1039,11 @@ pub(crate) enum EventPatternValueIr { /// A nullary sum-variant tag, resolved and unqualified — bare /// `tag: String` mirrors `IrPat::Variant`'s own `tag`/[`GlobalRef`]'s /// own `tag`. The AST's own optional qualifying `type_name` is - /// dropped, not lost: the sole existing consumer (the shipped - /// emitter's `event_pattern_guard`) already destructures down to the - /// bare tag alone — the qualification is disambiguation for the - /// *checker*, resolved against the field's declared sum type before - /// this point. + /// dropped, not lost: the sole consumer + /// ([`crate::emitter::lower::event_pattern_guard_ir`], #1187's slice 5) + /// already destructures down to the bare tag alone — the qualification + /// is disambiguation for the *checker*, resolved against the field's + /// declared sum type before this point. Variant { tag: String }, } @@ -1458,6 +1458,15 @@ pub(crate) struct IrHandler { pub connection: Option, pub body: IrExpr, pub commit: CommitShape, + /// The handler's own declared return type, resolved (#1187's slice 5, + /// the `Service` emitter cutover) — mirrors [`IrItem::Fn::ret`]'s + /// identical field, added here + /// for the identical reason: [`lower::lower_handler_signature_ir`] + /// already resolved this value to compute `effectful` below and + /// discarded it, leaving a service emitter with no IR-native way to + /// render a handler's own return-type annotation without re-walking + /// `Handler::return_type` (`bynk_syntax::ast::TypeRef`) itself. + pub ret: TyId, pub effectful: bool, pub method_name: Option, } diff --git a/bynk-emit/src/ir/lower.rs b/bynk-emit/src/ir/lower.rs index 4ad479795..6595aa6ad 100644 --- a/bynk-emit/src/ir/lower.rs +++ b/bynk-emit/src/ir/lower.rs @@ -404,7 +404,7 @@ pub(crate) fn lower_handler_ir( service handler reaching the wrong entry point" ); let cx = LowerIrCtx::new(program, HashSet::new()); - let (params, given, effectful) = lower_handler_signature_ir(h, &cx); + let (params, given, ret, effectful) = lower_handler_signature_ir(h, &cx); let emits = block_uses_emit(&h.body); let commit = lower_commit_shape_ir(&h.body, invariants, transitions, emits, program); let body = lower_handler_body_ir(h, store_cells, state_ty, program); @@ -423,11 +423,19 @@ pub(crate) fn lower_handler_ir( connection: None, body, commit, + ret, effectful, method_name: h.method_name.as_ref().map(|i| i.name.clone()), } } +/// `(params, given, ret, effectful)` — [`lower_handler_signature_ir`]'s own +/// return shape, and [`lower_service_handler_signature_ir`]'s (#1187's slice +/// 5), reused as a named alias rather than a bare tuple at both call sites +/// once one of them (`emit_service`, `bynk-emit/src/emitter/emit.rs`) had to +/// spell it out in a function signature. +pub(crate) type HandlerSignatureIr = (Vec<(String, TyId)>, Vec, TyId, bool); + /// A handler's own `params`/`given`/`effectful` — the one part of /// [`IrHandler`] construction genuinely identical between an agent handler /// ([`lower_handler_ir`]) and a service handler @@ -436,10 +444,7 @@ pub(crate) fn lower_handler_ir( /// `binder`, the WebSocket deferral — is genuinely different and stays /// unshared; see [`lower_handler_ir`]'s own doc comment for why that split /// is deliberate, not an oversight. -fn lower_handler_signature_ir( - h: &Handler, - cx: &LowerIrCtx, -) -> (Vec<(String, TyId)>, Vec, bool) { +fn lower_handler_signature_ir(h: &Handler, cx: &LowerIrCtx) -> HandlerSignatureIr { let params: Vec<(String, TyId)> = h .params .iter() @@ -463,7 +468,74 @@ fn lower_handler_signature_ir( ) }); let effectful = matches!(&*cx.program.ty_intern.get(ret), Ty::Effect(_)); - (params, given, effectful) + (params, given, ret, effectful) +} + +/// #1187's slice 5 (the `Service` emitter cutover): `emit_service`'s own +/// standalone entry point for a handler's resolved *signature* only — +/// `params`/`ret`/`effectful`, never the body. Deliberately does not build +/// a real [`IrHandler`]/[`crate::ir::IrItem::Service`]: both +/// [`lower_handler_ir`]/[`lower_service_handler_ir`] unconditionally lower +/// the handler's own body into a real `IrExpr` (`IrHandler::body` is not +/// `Option`), and an ordinary `from http` handler's body routinely +/// constructs `Ok`/`Err`/`Some`/`None` (an `HttpResult`/`Option` return) — +/// still `todo!()` in `lower_expr_ir` (P6.2/P6.3, #1143/#1145 — a gap +/// independent of #1189's own BinOp/Neg/InterpStr fix, see that issue's own +/// "confirmed independent" finding). Building a real `IrHandler` at +/// `emit_service`'s own call site would panic on exactly the ordinary Http +/// services this slice needs to keep working. Mirrors +/// [`body_writes_state`]'s own precedent (#1196): a narrow, standalone +/// reader of already-resolved data, not the full `IrItem`/`IrHandler` +/// assembly — the same posture, applied to signature data instead of a +/// single boolean. +/// Deliberately **not** `lower_handler_signature_ir(h, &cx)` (review of +/// #1198) — that helper's own ADR 0334 `.unwrap_or_else(|| panic!(..))` on a +/// resolution miss is correct for an *agent* handler (the checker +/// guarantees resolution there) but not for a *service* one: +/// `resolver.rs` skips `CommonsItem::Service` in every type-ref-resolution +/// pass, `check_handler_body` silently skips a param whose type doesn't +/// resolve and silently returns on an unresolvable return type (no +/// diagnostic either way), and `check_http_handler` only constrains a +/// param's *name* (path segment or `body`), never validates a `body:` +/// param's own declared type. A service handler naming an undeclared type +/// certifies today (and previously just emitted that bad name verbatim, a +/// `tsc`-only failure) — reusing the strict helper here would turn that +/// pre-existing, real-but-harmless-to-the-compiler gap into an ICE on the +/// production emit path for every service in every project (confirmed live: +/// `on POST("/x") (body: Nope) -> Effect[HttpResult[String]] by v: Visitor +/// { ... }` panics `bynkc` before this fix). Mirrors [`lower_protocol_ir`]'s +/// own `Ty::Unit`-on-miss posture, for the identical underlying reason (that +/// function's own doc comment already documents the checker's Service-wide +/// resolution gap). +/// +/// `effectful` is computed from `h.return_type`'s own AST shape +/// (`TypeRef::Effect(..)`, matching [`crate::emitter::is_effectful_return`] +/// exactly), not from the *resolved* `ret`'s `Ty::Effect(_)` shape — a +/// resolution miss on `ret` (falling back to `Ty::Unit` below) must not +/// silently flip an `Effect[Nope]`-returning handler to non-effectful; the +/// top-level `Effect[...]` wrapper is always syntactically determinable +/// regardless of whether its own inner type resolves. +pub(crate) fn lower_service_handler_signature_ir( + h: &Handler, + program: &CheckedProgram, +) -> HandlerSignatureIr { + let cx = LowerIrCtx::new(program, HashSet::new()); + let params: Vec<(String, TyId)> = h + .params + .iter() + .map(|p| { + let ty = cx + .resolve_type_ref(&p.type_ref) + .unwrap_or_else(|| cx.unit_ty()); + (p.name.name.clone(), ty) + }) + .collect(); + let given: Vec = h.given.iter().map(|c| c.key().to_string()).collect(); + let ret = cx + .resolve_type_ref(&h.return_type) + .unwrap_or_else(|| cx.unit_ty()); + let effectful = crate::emitter::is_effectful_return(&h.return_type); + (params, given, ret, effectful) } /// P6.11 ([DECISION E], #1171): lower a service handler's own body — the @@ -583,7 +655,7 @@ pub(crate) fn lower_service_handler_ir( program: &CheckedProgram, ) -> IrHandler { let cx = LowerIrCtx::new(program, HashSet::new()); - let (params, given, effectful) = lower_handler_signature_ir(h, &cx); + let (params, given, ret, effectful) = lower_handler_signature_ir(h, &cx); // Read straight off `h.by_clause`, not derived from `binder` below — // review of #1180: `binder` alone loses the gate itself for a // binder-less `by ` (`ActorBinder`'s own doc comment already @@ -635,6 +707,7 @@ pub(crate) fn lower_service_handler_ir( connection, body, commit, + ret, effectful, method_name: h.method_name.as_ref().map(|i| i.name.clone()), } @@ -6638,13 +6711,54 @@ service Outbox from queue("orders") { )); let handler = find_service_handler(service, &HandlerKind::Message); let cx = LowerIrCtx::new(&program, HashSet::new()); - let (params, given, effectful) = lower_handler_signature_ir(handler, &cx); + let (params, given, _ret, effectful) = lower_handler_signature_ir(handler, &cx); assert_eq!(params.len(), 1); assert_eq!(params[0].0, "m"); assert!(given.is_empty()); assert!(effectful, "every service handler returns Effect[T]"); } + /// #1187's slice 5 (the `Service` emitter cutover, review of #1196): + /// `lower_service_handler_signature_ir` is `emit_service`'s own real + /// call site's entry point, not `lower_handler_signature_ir` directly — + /// this pins it against the exact shape that motivated it: an ordinary + /// `from http` handler body constructing `Ok(...)` directly (not routed + /// through the `fn ok(s) -> HttpResult[String] { Ok(s) }` indirection + /// every other fixture in this module uses to dodge P6.2/P6.3's own + /// still-open `Ok`/`Err`/`Some`/`None` gap, #1143/#1145). Building a real + /// `IrHandler` here (`lower_service_handler_ir`) would panic on this + /// exact body — `lower_service_handler_signature_ir` never touches it. + #[test] + fn service_handler_signature_lowers_without_touching_a_body_that_constructs_ok() { + let program = checked_context_program( + r#" +context demo + +service Api from http { + on GET("/ping") () -> Effect[HttpResult[String]] by v: Visitor { + Effect.pure(Ok("pong")) + } +} +"#, + ); + let service = find_service(&program, "Api"); + let handler = find_service_handler( + service, + &HandlerKind::Http { + method: bynk_syntax::ast::HttpMethod::Get, + path: "/ping".to_string(), + }, + ); + let (params, _given, ret, effectful) = + lower_service_handler_signature_ir(handler, &program); + assert!(params.is_empty(), "`() -> ...` declares no parameters"); + assert!(effectful, "an `Effect[...]` return type"); + assert!(matches!( + &*program.program().ty_intern.get(ret), + Ty::Effect(_) + )); + } + #[test] #[should_panic(expected = "neither a locally-bound name")] fn a_queue_services_on_message_handler_reaches_ordinary_body_lowering_not_the_websocket_deferral() diff --git a/bynkc/tests/fixtures/positive/1199_service_handler_unresolvable_param_type_no_ice/expected/demo.ts b/bynkc/tests/fixtures/positive/1199_service_handler_unresolvable_param_type_no_ice/expected/demo.ts new file mode 100644 index 000000000..870de0237 --- /dev/null +++ b/bynkc/tests/fixtures/positive/1199_service_handler_unresolvable_param_type_no_ice/expected/demo.ts @@ -0,0 +1,19 @@ +// Generated by bynkc — do not edit by hand. +// context demo + +import { Ok, Err, Some, None, type Result, type Option, type ValidationError, HttpResult } from "./runtime.js"; + +export const Api = { + async http_POST_x(body: void, deps: {}): Promise> { + return HttpResult.Ok("hi"); + }, +}; + +export interface DemoDeps { +} + +export function makeSurface(deps: DemoDeps) { + return { + }; +} + diff --git a/bynkc/tests/fixtures/positive/1199_service_handler_unresolvable_param_type_no_ice/src/demo.bynk b/bynkc/tests/fixtures/positive/1199_service_handler_unresolvable_param_type_no_ice/src/demo.bynk new file mode 100644 index 000000000..f60c89b2e --- /dev/null +++ b/bynkc/tests/fixtures/positive/1199_service_handler_unresolvable_param_type_no_ice/src/demo.bynk @@ -0,0 +1,19 @@ +context demo + +-- Regression (review of #1198): a service handler naming an undeclared +-- type in a param certifies today — the checker's own resolution passes +-- (`resolver.rs`) skip `CommonsItem::Service` entirely, `check_handler_body` +-- silently skips a param whose type doesn't resolve, and `check_http_handler` +-- only validates a param's *name* (path segment or `body`), never its +-- declared type. Before this slice's own review fix, `emit_service`'s new +-- IR-driven signature lowering panicked (`bynk internal error (ADR 0334)`) +-- on exactly this shape — a real ICE on a program that previously just +-- emitted the bad name verbatim (a `tsc`-only failure). This fixture pins +-- that it no longer crashes the compiler: `Nope` (undeclared) falls back to +-- `void`, matching `lower_protocol_ir`'s own established Ty::Unit-on-miss +-- posture for the identical underlying checker gap. +service Api from http { + on POST("/x") (body: Nope) -> Effect[HttpResult[String]] by v: Visitor { + Effect.pure(Ok("hi")) + } +} diff --git a/design/pending/p6-service-signature-cutover.md b/design/pending/p6-service-signature-cutover.md new file mode 100644 index 000000000..0c624df6e --- /dev/null +++ b/design/pending/p6-service-signature-cutover.md @@ -0,0 +1,4 @@ +--- +level: patch +changelog: "emit_service now reads a handler's resolved signature (params/return type/effectful-ness) and a service's protocol data from bynk-emit::ir instead of walking bynk_syntax::ast TypeRefs/ServiceProtocol directly (internal only, byte-identical output — no language surface change)" +---