use super::{CodecTarget, Constant, Handle, Handoff, Routine, Scalar};
use crate::encode::EncodeError;
use zerodds_ast::{Ast, MemberNode, TypeNode};
use zerodds_ast_core::Id;
use zerodds_lang_cst::common::{Doc, Ident, Literal, Path};
use zerodds_lang_cst::typescript as ts;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TypeScript;
impl TypeScript {
fn camel(method: &str) -> String {
let mut out = String::new();
let mut rising = false;
for character in method.chars() {
if character == '_' {
rising = true;
continue;
}
if rising {
out.extend(character.to_uppercase());
rising = false;
} else {
out.push(character);
}
}
out
}
fn path(name: &str) -> Path {
Path::new(&name.split('.').collect::<Vec<_>>())
}
fn reading_body(name: &str, sample: ts::Type, body: ts::Block) -> ts::Block {
if let Some((key, value)) = readonly_map_halves(&sample) {
return Self::reading_map_body(key, value, body);
}
let defaulter = crate::default_typescript::defaulter_from_decoder(name);
let mut statements = vec![ts::Statement::Declare {
kind: ts::VarKind::Let,
name: Ident::new("v"),
type_: Some(sample),
value: Some(Self::call(&defaulter, Vec::new())),
}];
statements.extend(body.statements);
statements.push(ts::Statement::Return(Some(Self::whole("v"))));
ts::Block { statements }
}
fn reading_map_body(key: ts::Type, value: ts::Type, body: ts::Block) -> ts::Block {
let mut statements = vec![ts::Statement::Declare {
kind: ts::VarKind::Let,
name: Ident::new("v"),
type_: Some(ts::Type::Array(Box::new(entry_object(key, value)))),
value: Some(ts::Expr::Array(Vec::new())),
}];
statements.extend(body.statements.into_iter().map(answer_with_map));
statements.push(ts::Statement::Return(Some(map_of_entries())));
ts::Block { statements }
}
}
fn entry_object(key: ts::Type, value: ts::Type) -> ts::Type {
let property = |name: &str, type_: ts::Type| {
ts::Signature::Property(ts::Property {
doc: Doc::none(),
readonly: false,
name: ts::PropertyName::ident(name),
optional: false,
type_,
})
};
ts::Type::Object(vec![property("key", key), property("value", value)])
}
fn map_of_entries() -> ts::Expr {
let half = |name: &str| ts::Expr::Member {
object: Box::new(ts::Expr::Path(Path::new(&["zd_p"]))),
name: Ident::new(name),
};
ts::Expr::New {
callee: Path::new(&["Map"]),
args: vec![ts::Expr::Call {
callee: Box::new(ts::Expr::Member {
object: Box::new(ts::Expr::Path(Path::new(&["v"]))),
name: Ident::new("map"),
}),
args: vec![ts::Expr::Arrow {
params: vec![Ident::new("zd_p")],
body: Box::new(ts::Expr::AsConst(Box::new(ts::Expr::Array(vec![
half("key"),
half("value"),
])))),
}],
}],
}
}
fn entries_of_map() -> ts::Expr {
entries_of(ts::Expr::Path(Path::new(&["zd_map"])))
}
fn entries_of(source: ts::Expr) -> ts::Expr {
let half = |index: i128| ts::Expr::Index {
object: Box::new(ts::Expr::Path(Path::new(&["zd_p"]))),
index: Box::new(ts::Expr::Literal(Literal::int(index))),
};
ts::Expr::Call {
callee: Box::new(ts::Expr::Member {
object: Box::new(ts::Expr::Call {
callee: Box::new(ts::Expr::Member {
object: Box::new(ts::Expr::Path(Path::new(&["Array"]))),
name: Ident::new("from"),
}),
args: vec![source],
}),
name: Ident::new("map"),
}),
args: vec![ts::Expr::Arrow {
params: vec![Ident::new("zd_p")],
body: Box::new(ts::Expr::Parenthesised(Box::new(ts::Expr::Object(vec![
(ts::PropertyName::ident("key"), half(0)),
(ts::PropertyName::ident("value"), half(1)),
])))),
}],
}
}
fn readonly_map_halves(type_: &ts::Type) -> Option<(ts::Type, ts::Type)> {
let ts::Type::Named { path, args } = type_ else {
return None;
};
let [key, value] = args.as_slice() else {
return None;
};
let [head] = path.segments() else {
return None;
};
if head.as_str() != "ReadonlyMap" {
return None;
}
Some((key.clone(), value.clone()))
}
fn answers_a_map(value: &ts::Expr) -> bool {
let ts::Expr::Call { callee, .. } = value else {
return false;
};
let ts::Expr::Path(path) = callee.as_ref() else {
return false;
};
path.segments()
.last()
.is_some_and(|segment| segment.as_str().starts_with("zd_def_"))
}
fn answer_with_map(statement: ts::Statement) -> ts::Statement {
let rewrite = |block: ts::Block| ts::Block {
statements: block.statements.into_iter().map(answer_with_map).collect(),
};
match statement {
ts::Statement::Return(Some(ts::Expr::Path(ref path)))
if path.segments().len() == 1
&& path
.segments()
.first()
.is_some_and(|segment| segment.as_str() == "v") =>
{
ts::Statement::Return(Some(map_of_entries()))
}
ts::Statement::Expr(ts::Expr::Binary { op: "=", lhs, rhs })
if matches!(
lhs.as_ref(),
ts::Expr::Path(path)
if path.segments().len() == 1
&& path
.segments()
.first()
.is_some_and(|segment| segment.as_str() == "v")
) && answers_a_map(rhs.as_ref()) =>
{
ts::Statement::Expr(ts::Expr::Binary {
op: "=",
lhs,
rhs: Box::new(entries_of(*rhs)),
})
}
ts::Statement::If {
condition,
then,
otherwise,
} => ts::Statement::If {
condition,
then: rewrite(then),
otherwise: otherwise.map(rewrite),
},
ts::Statement::For {
counter,
bound,
body,
} => ts::Statement::For {
counter,
bound,
body: rewrite(body),
},
ts::Statement::While { condition, body } => ts::Statement::While {
condition,
body: rewrite(body),
},
ts::Statement::ForOf {
kind,
binding,
range,
body,
} => ts::Statement::ForOf {
kind,
binding,
range,
body: rewrite(body),
},
ts::Statement::Switch { value, cases } => ts::Statement::Switch {
value,
cases: cases
.into_iter()
.map(|case| ts::SwitchCase {
labels: case.labels,
body: rewrite(case.body),
breaks: case.breaks,
})
.collect(),
},
other => other,
}
}
impl CodecTarget for TypeScript {
type Expr = ts::Expr;
type Stmt = ts::Statement;
type Block = ts::Block;
type Type = ts::Type;
type Decl = ts::Decl;
fn scalar(kind: Scalar) -> Self::Type {
match kind {
Scalar::Bool => ts::Type::Keyword(ts::Keyword::Boolean),
Scalar::Char => ts::Type::string(),
Scalar::Int8
| Scalar::UInt8
| Scalar::Int16
| Scalar::UInt16
| Scalar::Int32
| Scalar::UInt32
| Scalar::Index
| Scalar::Size
| Scalar::Float
| Scalar::Double => ts::Type::number(),
Scalar::Int64 | Scalar::UInt64 => ts::Type::bigint(),
Scalar::LongDouble => ts::Type::named(Path::new(&["ZdFloat128"])),
}
}
fn named(name: &str) -> Self::Type {
ts::Type::named(Self::path(name))
}
fn nothing() -> Self::Type {
ts::Type::Keyword(ts::Keyword::Void)
}
fn handle(kind: Handle) -> Self::Type {
ts::Type::named(Path::new(&[match kind {
Handle::DHeader => "DHeader",
Handle::EmHeader => "EmHeader",
Handle::Pid => "Pid",
Handle::TryConstruct => "TryConstruct",
Handle::ReaderMark => "ReaderMark",
Handle::PidRead => "PidRead",
Handle::EmHeaderRead => "EmHeaderRead",
}]))
}
fn code_point(value: Self::Expr, _narrow: bool) -> Self::Expr {
value
}
fn constant(which: Constant) -> Self::Expr {
ts::Expr::Path(Self::path(match which {
Constant::WcharWidth2 => "WcharWidth.Octets2",
Constant::WcharWidth4 => "WcharWidth.Octets4",
Constant::WcharFixed => "WcharFraming.Fixed",
Constant::WcharGiopLengthPrefixed => "WcharFraming.GiopLengthPrefixed",
Constant::WstringUnitsStream => "WstringUnitOrder.Stream",
Constant::WstringUnitsBigEndianAlways => "WstringUnitOrder.BigEndianAlways",
Constant::WstringUnitsByteOrderMark => "WstringUnitOrder.ByteOrderMark",
Constant::WstringUnitsHostUnswapped => "WstringUnitOrder.HostUnswapped",
Constant::WstringSpecification => "WstringLengthPolicy.Specification",
Constant::WstringCharacterCount => "WstringLengthPolicy.CharacterCount",
Constant::WstringCharacterCountTerminated => {
"WstringLengthPolicy.CharacterCountTerminated"
}
Constant::WstringSpecificationWideUnits => "WstringLengthPolicy.SpecificationWideUnits",
Constant::WstringRosTypeSupport => "WstringLengthPolicy.RosTypeSupport",
Constant::WstringOctetsTerminated2 => "WstringLengthPolicy.OctetsTerminated2",
Constant::WstringOctetsTerminated4 => "WstringLengthPolicy.OctetsTerminated4",
Constant::WstringCharactersTerminated2 => "WstringLengthPolicy.CharactersTerminated2",
Constant::FixedDeclared => "FixedDigitCount.Declared",
Constant::FixedValuePrecision => "FixedDigitCount.ValuePrecision",
Constant::FixedValueStringDigits => "FixedDigitCount.ValueStringDigits",
Constant::ParameterMember => "ParameterRole.Member",
Constant::ParameterOptional => "ParameterRole.Optional",
Constant::TryConstructDiscard => "TryConstruct.Discard",
Constant::TryConstructTrim => "TryConstruct.Trim",
Constant::TryConstructUseDefault => "TryConstruct.UseDefault",
Constant::StatusOk => "ZdReaderStatus.Ok",
Constant::StatusDiscarded => "ZdReaderStatus.Discarded",
Constant::StatusImpossibleLength => "ZdReaderStatus.ImpossibleLength",
Constant::StatusOutOfRange => "ZdReaderStatus.OutOfRange",
Constant::StatusTruncated => "ZdReaderStatus.Truncated",
}))
}
fn unsigned_holder(bits: u16) -> Self::Type {
crate::adapter::typescript::unsigned_holder(bits)
}
fn bitmask_value(ast: &Ast, id: Id<TypeNode>, value: Self::Expr) -> Self::Expr {
let bound = ast.effective_bit_bound(id);
Self::cast(
Self::value_type(ast, id),
Self::cast(Self::unsigned_holder(bound), value),
)
}
fn value_type(ast: &Ast, id: Id<TypeNode>) -> Self::Type {
value_type_of(ast, id, 0)
}
fn type_name(ast: &Ast, id: Id<TypeNode>) -> String {
crate::adapter::typescript::type_tag(ast, id, 0)
.unwrap_or_else(|_| format!("unresolved_type_{}", id.slot()))
}
fn map_entry_name(ast: &Ast, id: Id<TypeNode>) -> String {
crate::adapter::typescript::map_entry_name(ast, id)
.unwrap_or_else(|_| format!("UnresolvedEntry{}", id.slot()))
}
fn module_prefix(_ast: &Ast, _id: Id<TypeNode>) -> String {
String::new()
}
fn escape(name: &str) -> String {
ts::escape_member(name)
}
fn inherited_path(depth: usize) -> Option<String> {
Some("parent.".repeat(depth))
}
fn name(text: &str) -> Self::Expr {
ts::Expr::Path(Self::path(text))
}
fn int(value: i128) -> Self::Expr {
ts::Expr::Literal(Literal::int(value))
}
fn wide_int(value: i128) -> Self::Expr {
ts::Expr::Literal(Literal::Int(format!("{value}n")))
}
fn literal(value: Literal) -> Self::Expr {
ts::Expr::Literal(value)
}
fn unsigned_int(value: i128) -> Self::Expr {
ts::Expr::Literal(Literal::int(value))
}
fn header_field(value: Self::Expr, field: &str) -> Self::Expr {
Self::field_of(
value,
match field {
"pid" => "pid",
"end" => "end",
"ignore" => "ignore",
"length" => "length",
"member_id" => "memberId",
"must_understand" => "mustUnderstand",
"extent" => "extent",
other => other,
},
)
}
fn call(callee: &str, args: Vec<Self::Expr>) -> Self::Expr {
if callee == "strcmp" {
if let [left, right] = args.as_slice() {
return ts::Expr::Paren(Box::new(ts::Expr::Ternary {
condition: Box::new(ts::Expr::Binary {
op: "<",
lhs: Box::new(left.clone()),
rhs: Box::new(right.clone()),
}),
then: Box::new(Self::int(-1)),
otherwise: Box::new(ts::Expr::Ternary {
condition: Box::new(ts::Expr::Binary {
op: ">",
lhs: Box::new(left.clone()),
rhs: Box::new(right.clone()),
}),
then: Box::new(Self::int(1)),
otherwise: Box::new(Self::int(0)),
}),
}));
}
}
ts::Expr::Call {
callee: Box::new(ts::Expr::Path(Self::path(callee))),
args,
}
}
fn runtime(receiver: &str, method: &str, args: Vec<Self::Expr>) -> Self::Expr {
ts::Expr::Call {
callee: Box::new(ts::Expr::Member {
object: Box::new(ts::Expr::Path(Self::path(receiver))),
name: Ident::new(&Self::camel(method)),
}),
args,
}
}
fn carries_hdds_axes() -> bool {
true
}
fn runtime_static(method: &str, args: Vec<Self::Expr>) -> Self::Expr {
Self::call(&Self::camel(method), args)
}
fn member(base: &str, field: &str) -> Self::Expr {
ts::Expr::Member {
object: Box::new(ts::Expr::Path(Self::path(base))),
name: Ident::new(field),
}
}
fn whole(base: &str) -> Self::Expr {
ts::Expr::Path(Self::path(base))
}
fn field_of(value: Self::Expr, field: &str) -> Self::Expr {
ts::Expr::Member {
object: Box::new(value),
name: Ident::new(field),
}
}
fn index(value: Self::Expr, at: Self::Expr) -> Self::Expr {
ts::Expr::NonNull(Box::new(ts::Expr::Index {
object: Box::new(value),
index: Box::new(at),
}))
}
fn count_of(base: &str, fixed: Option<u64>) -> Self::Expr {
match fixed {
Some(bound) => ts::Expr::Literal(Literal::int(i128::from(bound))),
None => ts::Expr::Member {
object: Box::new(Self::whole(base)),
name: Ident::new("length"),
},
}
}
fn elements(base: &str) -> Self::Expr {
Self::whole(base)
}
fn case_member(base: &str, field: &str) -> Self::Expr {
let arm = ts::Type::generic(
Path::new(&["Extract"]),
vec![
ts::Type::TypeOf(Path::new(&[base])),
ts::Type::Object(vec![ts::Signature::Property(ts::Property {
doc: Doc::none(),
readonly: false,
name: ts::PropertyName::ident(field),
optional: false,
type_: ts::Type::Keyword(ts::Keyword::Unknown),
})]),
],
);
ts::Expr::Member {
object: Box::new(ts::Expr::Paren(Box::new(ts::Expr::As {
value: Box::new(Self::whole(base)),
type_: Box::new(arm),
}))),
name: Ident::new(field),
}
}
fn entry_half(entry: Self::Expr, key: bool) -> Self::Expr {
ts::Expr::Member {
object: Box::new(entry),
name: Ident::new(if key { "key" } else { "value" }),
}
}
fn discriminant(base: &str) -> Self::Expr {
Self::member(base, crate::adapter::typescript::DISCRIMINANT_PROPERTY)
}
fn enumerator_ordinal(ast: &Ast, id: Id<TypeNode>, value: Self::Expr) -> Self::Expr {
let Ok(table) = crate::adapter::typescript::ordinal_path(ast, id) else {
return value;
};
ts::Expr::Index {
object: Box::new(ts::Expr::Path(table)),
index: Box::new(value),
}
}
fn case_label(ast: &Ast, discriminator: Id<TypeNode>, value: i128) -> Self::Expr {
crate::default_typescript::case_label(ast, discriminator, value).unwrap_or_else(|| {
Self::cast(Self::value_type(ast, discriminator), Self::wide_int(value))
})
}
fn absent() -> Self::Expr {
ts::Expr::Path(Path::new(&["undefined"]))
}
fn materialised(value: Self::Expr) -> Option<Self::Expr> {
Some(ts::Expr::Binary {
op: "!==",
lhs: Box::new(value),
rhs: Box::new(Self::absent()),
})
}
fn optional_value(_ast: &Ast, _id: Id<TypeNode>, base: &str, field: &str) -> Self::Expr {
Self::member(base, field)
}
fn present(base: &str, field: &str) -> Self::Expr {
ts::Expr::Binary {
op: "!==",
lhs: Box::new(Self::member(base, field)),
rhs: Box::new(Self::absent()),
}
}
fn set_present(base: &str, field: &str, present: bool) -> Vec<Self::Stmt> {
if present {
return Vec::new();
}
vec![ts::Statement::Expr(ts::Expr::Unary {
op: "delete ",
value: Box::new(Self::member(base, field)),
})]
}
fn clear_union_cases(base: &str, fields: &[String]) -> Vec<Self::Stmt> {
fields
.iter()
.map(|field| {
let optional = ts::Type::Object(vec![ts::Signature::Property(ts::Property {
doc: Doc::none(),
readonly: false,
name: ts::PropertyName::ident(field),
optional: true,
type_: ts::Type::Keyword(ts::Keyword::Unknown),
})]);
ts::Statement::Expr(ts::Expr::Unary {
op: "delete ",
value: Box::new(ts::Expr::Member {
object: Box::new(ts::Expr::Paren(Box::new(ts::Expr::As {
value: Box::new(Self::whole(base)),
type_: Box::new(optional),
}))),
name: Ident::new(field),
}),
})
})
.collect()
}
fn optional_destination(base: &str, field: &str) -> (Vec<Self::Stmt>, Self::Expr) {
(Vec::new(), Self::member(base, field))
}
fn binary(op: &'static str, lhs: Self::Expr, rhs: Self::Expr) -> Self::Expr {
ts::Expr::Binary {
op: match op {
"==" => "===",
"!=" => "!==",
other => other,
},
lhs: Box::new(lhs),
rhs: Box::new(rhs),
}
}
fn unary(op: &'static str, value: Self::Expr) -> Self::Expr {
ts::Expr::Unary {
op,
value: Box::new(value),
}
}
fn paren(value: Self::Expr) -> Self::Expr {
ts::Expr::Paren(Box::new(value))
}
fn bool_less(left: Self::Expr, right: Self::Expr) -> Self::Expr {
Self::binary(
"&&",
ts::Expr::Paren(Box::new(Self::binary(
"==",
left,
ts::Expr::Literal(Literal::Bool(false)),
))),
right,
)
}
fn cast(type_: Self::Type, value: Self::Expr) -> Self::Expr {
if let ts::Expr::Literal(Literal::Int(text)) = &value {
if let Some(literal) = literal_as(&type_, text) {
return ts::Expr::Literal(literal);
}
}
if type_ == ts::Type::named(Self::path("signed char")) {
let point = ts::Expr::Call {
callee: Box::new(ts::Expr::Member {
object: Box::new(ts::Expr::Paren(Box::new(value))),
name: Ident::new("charCodeAt"),
}),
args: vec![Self::int(0)],
};
return Self::binary(
"-",
ts::Expr::Paren(Box::new(Self::binary(
"%",
ts::Expr::Paren(Box::new(Self::binary("+", point, Self::int(128)))),
Self::int(256),
))),
Self::int(128),
);
}
let coercion = match &type_ {
ts::Type::Keyword(ts::Keyword::Number) => Some("zdNumber"),
ts::Type::Keyword(ts::Keyword::BigInt) => Some("zdBigInt"),
ts::Type::Keyword(ts::Keyword::String) => Some("zdChar"),
_ => None,
};
match coercion {
Some(callee) if is_call_to(&value, callee) => value,
Some(callee) => Self::call(callee, vec![value]),
None => ts::Expr::As {
value: Box::new(value),
type_: Box::new(type_),
},
}
}
fn store(place: Self::Expr, type_: Self::Type, value: Self::Expr) -> Self::Stmt {
Self::perform(Self::binary(
"=",
assignable(place),
Self::cast(type_, value),
))
}
fn handoff(value: Self::Expr, _form: Handoff) -> Self::Expr {
value
}
fn read_call(
callee: &str,
place: Self::Expr,
_form: Handoff,
behaviour: Self::Expr,
) -> Self::Stmt {
Self::perform(Self::binary(
"=",
assignable(place),
Self::call(callee, vec![Self::name("r"), behaviour]),
))
}
fn read_forward(callee: &str) -> Self::Stmt {
Self::read_call(
callee,
Self::whole("v"),
Handoff::Value,
Self::name("zd_tc"),
)
}
fn declare(type_: Self::Type, name: &str, value: Option<Self::Expr>) -> Self::Stmt {
let value = value.map(|value| match &value {
ts::Expr::Literal(Literal::Int(text)) => {
literal_as(&type_, text).map_or_else(|| value.clone(), ts::Expr::Literal)
}
_ => value,
});
ts::Statement::Declare {
kind: ts::VarKind::Let,
name: Ident::new(name),
type_: Some(type_),
value,
}
}
fn perform(value: Self::Expr) -> Self::Stmt {
ts::Statement::Expr(value)
}
fn give_back(value: Option<Self::Expr>) -> Self::Stmt {
ts::Statement::Return(Some(value.unwrap_or_else(|| Self::whole("v"))))
}
fn mark_used(_name: &str) -> Vec<Self::Stmt> {
Vec::new()
}
fn drop_value(value: Self::Expr) -> Self::Stmt {
ts::Statement::Expr(ts::Expr::Unary {
op: "void ",
value: Box::new(value),
})
}
fn when(
condition: Self::Expr,
then: Self::Block,
otherwise: Option<Self::Block>,
) -> Self::Stmt {
ts::Statement::If {
condition,
then,
otherwise,
}
}
fn count_up(counter: &str, bound: Self::Expr, body: Self::Block) -> Self::Stmt {
ts::Statement::For {
counter: Ident::new(counter),
bound: Box::new(bound),
body,
}
}
fn until(condition: Self::Expr, body: Self::Block) -> Self::Stmt {
ts::Statement::While {
condition: Box::new(condition),
body,
}
}
fn leave() -> Self::Stmt {
ts::Statement::Break
}
fn note(doc: Doc) -> Self::Stmt {
ts::Statement::Comment(doc)
}
fn block(statements: Vec<Self::Stmt>) -> Self::Block {
ts::Block { statements }
}
fn into_statements(block: Self::Block) -> Vec<Self::Stmt> {
block.statements
}
fn make_room(
base: &str,
element: Self::Type,
count: Self::Expr,
assigned_whole: bool,
fill: Vec<Self::Stmt>,
) -> Vec<Self::Stmt> {
let mut out = vec![ts::Statement::Expr(ts::Expr::Binary {
op: "=",
lhs: Box::new(Self::whole(base)),
rhs: Box::new(ts::Expr::New {
callee: Path::new(&["Array"]),
args: vec![count],
}),
})];
if !assigned_whole {
out.push(Self::count_up(
"zd_e",
Self::count_of(base, None),
ts::Block {
statements: vec![ts::Statement::Expr(ts::Expr::Binary {
op: "=",
lhs: Box::new(ts::Expr::Index {
object: Box::new(Self::whole(base)),
index: Box::new(Self::whole("zd_e")),
}),
rhs: Box::new(ts::Expr::As {
value: Box::new(ts::Expr::Object(Vec::new())),
type_: Box::new(element),
}),
})],
},
));
}
out.extend(fill);
out
}
fn storage(type_: Self::Type, name: &str) -> Self::Stmt {
ts::Statement::Declare {
kind: ts::VarKind::Let,
name: Ident::new(name),
type_: Some(type_.clone()),
value: Some(ts::Expr::As {
value: Box::new(ts::Expr::Object(Vec::new())),
type_: Box::new(type_),
}),
}
}
fn keep_first(base: &str, count: Self::Expr) -> Vec<Self::Stmt> {
vec![ts::Statement::Expr(ts::Expr::Binary {
op: "=",
lhs: Box::new(ts::Expr::Member {
object: Box::new(Self::whole(base)),
name: Ident::new("length"),
}),
rhs: Box::new(count),
})]
}
fn sequence_element(
base: &str,
index: Self::Expr,
_element: Self::Type,
) -> (Vec<Self::Stmt>, Self::Expr, Vec<Self::Stmt>) {
(
Vec::new(),
Self::index(Self::elements(base), index),
Vec::new(),
)
}
fn routine(spec: Routine<Self>) -> Option<Self::Decl> {
let body = spec.body?;
let mut params = vec![ts::Param {
name: Ident::new(if spec.writing { "w" } else { "r" }),
type_: Some(ts::Type::named(Path::new(&[if spec.writing {
"Writer"
} else {
"Reader"
}]))),
optional: false,
default: None,
rest: false,
}];
let staged = spec
.writing
.then(|| readonly_map_halves(&spec.sample))
.flatten();
if spec.writing {
params.push(ts::Param {
name: Ident::new(if staged.is_some() { "zd_map" } else { "v" }),
type_: Some(spec.sample.clone()),
optional: false,
default: None,
rest: false,
});
}
params.extend(spec.extra.into_iter().map(|(type_, name)| ts::Param {
name: Ident::new(&name),
type_: Some(type_),
optional: false,
default: None,
rest: false,
}));
let (returns, body) = if spec.writing {
let body = match staged {
Some((key, value)) => {
let mut statements = vec![ts::Statement::Declare {
kind: ts::VarKind::Const,
name: Ident::new("v"),
type_: Some(ts::Type::Array(Box::new(entry_object(key, value)))),
value: Some(entries_of_map()),
}];
statements.extend(body.statements);
ts::Block { statements }
}
None => body,
};
(Self::nothing(), body)
} else {
(
spec.sample.clone(),
Self::reading_body(&spec.name, spec.sample, body),
)
};
Some(ts::Decl::Function(ts::Function {
doc: spec.doc,
exported: true,
name: Ident::new(&spec.name),
generics: Vec::new(),
params,
returns: Some(returns),
body: Some(body),
}))
}
fn default_into(
ast: &Ast,
id: Id<TypeNode>,
place: Self::Expr,
) -> Result<Self::Stmt, EncodeError> {
Ok(crate::default_typescript::default_into(ast, id, place))
}
fn member_default(
ast: &Ast,
node: &MemberNode,
type_: Id<TypeNode>,
place: Self::Expr,
) -> Result<Vec<Self::Stmt>, EncodeError> {
crate::default_typescript::member_default(ast, node, type_, place)
}
fn enumerator_constant(ast: &Ast, id: Id<TypeNode>, enumerator: &str) -> Self::Expr {
crate::default_typescript::enumerator_constant(ast, id, enumerator)
}
fn wide_unsigned(value: u64) -> Self::Expr {
Self::literal(Literal::Int(format!("{value}n")))
}
fn wide_signed(value: i128) -> Self::Expr {
Self::literal(Literal::Int(format!("{value}n")))
}
}
const MAX_TYPE_DEPTH: usize = 64;
fn assignable(place: ts::Expr) -> ts::Expr {
match place {
ts::Expr::NonNull(inner) => *inner,
other => other,
}
}
fn is_call_to(value: &ts::Expr, name: &str) -> bool {
let ts::Expr::Call { callee, .. } = value else {
return false;
};
let ts::Expr::Path(path) = callee.as_ref() else {
return false;
};
path.segments().len() == 1
&& path
.segments()
.first()
.is_some_and(|segment| segment.as_str() == name)
}
fn literal_as(target: &ts::Type, text: &str) -> Option<Literal> {
let digits = text.strip_suffix('n').unwrap_or(text);
let body = digits.strip_prefix('-').unwrap_or(digits);
if body.is_empty() || !body.bytes().all(|byte| byte.is_ascii_digit()) {
return None;
}
match target {
ts::Type::Keyword(ts::Keyword::BigInt) => Some(Literal::Int(format!("{digits}n"))),
ts::Type::Keyword(ts::Keyword::Boolean) => Some(Literal::Bool(digits != "0")),
ts::Type::Keyword(ts::Keyword::String) => digits
.parse::<u32>()
.ok()
.and_then(char::from_u32)
.map(Literal::Char),
ts::Type::Keyword(ts::Keyword::Number) | ts::Type::Named { .. } => {
Some(Literal::Int(digits.to_string()))
}
_ => None,
}
}
fn value_type_of(ast: &Ast, id: Id<TypeNode>, depth: usize) -> ts::Type {
use zerodds_ast::{PrimitiveKind, TypeKind};
let unknown = || ts::Type::Keyword(ts::Keyword::Unknown);
if depth > MAX_TYPE_DEPTH {
return unknown();
}
let Ok(node) = ast.type_(id) else {
return unknown();
};
let declared = || {
crate::adapter::typescript::declared_path(ast, id)
.map_or_else(|_| unknown(), ts::Type::named)
};
match &node.kind {
TypeKind::Primitive(kind) => match kind {
PrimitiveKind::Boolean => TypeScript::scalar(Scalar::Bool),
PrimitiveKind::Char | PrimitiveKind::WChar => TypeScript::scalar(Scalar::Char),
PrimitiveKind::Int64 | PrimitiveKind::UInt64 => TypeScript::scalar(Scalar::Int64),
PrimitiveKind::Float128 => TypeScript::scalar(Scalar::LongDouble),
_ => TypeScript::scalar(Scalar::Int32),
},
TypeKind::String { .. } | TypeKind::Fixed { .. } => ts::Type::string(),
TypeKind::Enumeration { .. } | TypeKind::Bitmask { .. } => declared(),
TypeKind::Bitset { .. }
| TypeKind::Structure { .. }
| TypeKind::Union { .. }
| TypeKind::Valuetype { .. }
| TypeKind::Native => declared(),
TypeKind::Sequence { element, .. } => {
let inner = element
.resolved()
.map_or_else(unknown, |target| value_type_of(ast, target, depth + 1));
ts::Type::Array(Box::new(inner))
}
TypeKind::Array { element, dims } => {
let mut out = element
.resolved()
.map_or_else(unknown, |target| value_type_of(ast, target, depth + 1));
for _ in dims {
out = ts::Type::Array(Box::new(out));
}
out
}
TypeKind::Map { key, value, .. } => {
let half = |half: &zerodds_ast_core::Ref<TypeNode>| {
half.resolved()
.map_or_else(unknown, |target| value_type_of(ast, target, depth + 1))
};
ts::Type::generic(Path::new(&["ReadonlyMap"]), vec![half(key), half(value)])
}
TypeKind::Alias { aliased } => aliased
.resolved()
.map_or_else(unknown, |target| value_type_of(ast, target, depth + 1)),
TypeKind::ValueBox { boxed } => {
let inner = boxed
.resolved()
.map_or_else(unknown, |target| value_type_of(ast, target, depth + 1));
ts::Type::Union(vec![inner, ts::Type::Keyword(ts::Keyword::Undefined)])
}
TypeKind::Any
| TypeKind::ValueBase
| TypeKind::ObjectRef { .. }
| TypeKind::AnnotationType { .. } => unknown(),
}
}