From 2ba2e04e0d3bba98efd9601a9829d74c6c3d7d49 Mon Sep 17 00:00:00 2001 From: Yota <91780796+wiyota@users.noreply.github.com> Date: Wed, 5 Nov 2025 12:15:04 +0900 Subject: [PATCH 1/4] feat: carry spans through IDL types for AST --- rust/candid_parser/src/bindings/motoko.rs | 94 +++++++---- rust/candid_parser/src/bindings/rust.rs | 62 +++---- rust/candid_parser/src/bindings/typescript.rs | 60 +++++-- rust/candid_parser/src/grammar.lalrpop | 154 +++++++++++++----- rust/candid_parser/src/syntax/mod.rs | 127 +++++++++------ rust/candid_parser/src/syntax/pretty.rs | 81 ++++----- rust/candid_parser/src/test.rs | 1 + rust/candid_parser/src/typing.rs | 44 +++-- rust/candid_parser/tests/parse_type.rs | 14 +- rust/candid_parser/tests/test_doc_comments.rs | 4 +- 10 files changed, 402 insertions(+), 239 deletions(-) diff --git a/rust/candid_parser/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs index 6302fde39..0c7d94197 100644 --- a/rust/candid_parser/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -1,7 +1,7 @@ // This module implements the Candid to Motoko binding as specified in // https://github.com/dfinity/motoko/blob/master/design/IDL-Motoko.md -use crate::syntax::{self, IDLActorType, IDLMergedProg, IDLType}; +use crate::syntax::{self, IDLActorType, IDLMergedProg, IDLType, IDLTypeKind}; use candid::pretty::candid::is_valid_as_id; use candid::pretty::utils::*; use candid::types::{Field, FuncMode, Function, Label, SharedLabel, Type, TypeInner}; @@ -97,24 +97,56 @@ fn escape(id: &str, is_method: bool) -> RcDoc<'_> { } fn pp_ty_rich<'a>(ty: &'a Type, syntax: Option<&'a IDLType>) -> RcDoc<'a> { - match (ty.as_ref(), syntax) { - (TypeInner::Service(ref meths), Some(IDLType::ServT(methods))) => { - pp_service(meths, Some(methods)) + match ty.as_ref() { + TypeInner::Service(ref meths) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::ServT(methods) = &syntax_ty.kind { + return pp_service(meths, Some(methods)); + } + } + pp_service(meths, None) } - (TypeInner::Class(ref args, t), Some(IDLType::ClassT(_, syntax_t))) => { - pp_class((args, t), Some(syntax_t)) + TypeInner::Class(ref args, t) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::ClassT(_, syntax_t) = &syntax_ty.kind { + return pp_class((args, t), Some(syntax_t)); + } + } + pp_class((args, t), None) + } + TypeInner::Record(ref fields) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::RecordT(syntax_fields) = &syntax_ty.kind { + return pp_record(fields, Some(syntax_fields)); + } + } + pp_record(fields, None) } - (TypeInner::Record(ref fields), Some(IDLType::RecordT(syntax_fields))) => { - pp_record(fields, Some(syntax_fields)) + TypeInner::Variant(ref fields) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::VariantT(syntax_fields) = &syntax_ty.kind { + return pp_variant(fields, Some(syntax_fields)); + } + } + pp_variant(fields, None) } - (TypeInner::Variant(ref fields), Some(IDLType::VariantT(syntax_fields))) => { - pp_variant(fields, Some(syntax_fields)) + TypeInner::Opt(ref inner) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::OptT(syntax_inner) = &syntax_ty.kind { + return str("?").append(pp_ty_rich(inner, Some(syntax_inner))); + } + } + str("?").append(pp_ty(inner)) } - (TypeInner::Opt(ref inner), Some(IDLType::OptT(syntax))) => { - str("?").append(pp_ty_rich(inner, Some(syntax))) + TypeInner::Vec(ref inner) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::VecT(syntax_inner) = &syntax_ty.kind { + return pp_vec(inner, Some(syntax_inner)); + } + } + pp_vec(inner, None) } - (TypeInner::Vec(ref inner), Some(IDLType::VecT(syntax))) => pp_vec(inner, Some(syntax)), - (_, _) => pp_ty(ty), + _ => pp_ty(ty), } } @@ -311,25 +343,25 @@ fn pp_actor<'a>(ty: &'a Type, syntax: Option<&'a IDLActorType>) -> RcDoc<'a> { let self_doc = kwd("public type Self ="); match ty.as_ref() { TypeInner::Service(ref serv) => match syntax { - Some(IDLActorType { - typ: IDLType::ServT(ref fields), - docs, - }) => { - let docs = pp_docs(docs); - docs.append(self_doc).append(pp_service(serv, Some(fields))) - } - _ => pp_service(serv, None), + Some(IDLActorType { typ, docs, .. }) => match &typ.kind { + IDLTypeKind::ServT(fields) => { + let docs = pp_docs(docs); + docs.append(self_doc).append(pp_service(serv, Some(fields))) + } + _ => pp_service(serv, None), + }, + None => pp_service(serv, None), }, TypeInner::Class(ref args, ref t) => match syntax { - Some(IDLActorType { - typ: IDLType::ClassT(_, syntax_t), - docs, - }) => { - let docs = pp_docs(docs); - docs.append(self_doc) - .append(pp_class((args, t), Some(syntax_t))) - } - _ => self_doc.append(pp_class((args, t), None)), + Some(IDLActorType { typ, docs, .. }) => match &typ.kind { + IDLTypeKind::ClassT(_, syntax_t) => { + let docs = pp_docs(docs); + docs.append(self_doc) + .append(pp_class((args, t), Some(syntax_t))) + } + _ => self_doc.append(pp_class((args, t), None)), + }, + None => self_doc.append(pp_class((args, t), None)), }, TypeInner::Var(_) => self_doc.append(pp_ty(ty)), _ => unreachable!(), diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index ef1439dab..b5b8c0af9 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -1,7 +1,7 @@ use super::analysis::{chase_actor, infer_rec}; use crate::{ configs::{ConfigState, ConfigTree, Configs, Context, StateElem}, - syntax::{self, IDLActorType, IDLMergedProg, IDLType}, + syntax::{self, IDLActorType, IDLMergedProg, IDLType, IDLTypeKind}, Deserialize, }; use candid::pretty::utils::*; @@ -157,19 +157,17 @@ fn find_field<'a>( } fn record_syntax_fields(syntax: Option<&IDLType>) -> Option<&[syntax::TypeField]> { - if let Some(IDLType::RecordT(syntax_fields)) = syntax { - Some(syntax_fields.as_slice()) - } else { - None - } + syntax.and_then(|ty| match &ty.kind { + IDLTypeKind::RecordT(fields) => Some(fields.as_slice()), + _ => None, + }) } fn variant_syntax_fields(syntax: Option<&IDLType>) -> Option<&[syntax::TypeField]> { - if let Some(IDLType::VariantT(syntax_fields)) = syntax { - Some(syntax_fields.as_slice()) - } else { - None - } + syntax.and_then(|ty| match &ty.kind { + IDLTypeKind::VariantT(fields) => Some(fields.as_slice()), + _ => None, + }) } fn actor_methods(actor: Option<&IDLActorType>) -> &[syntax::Binding] { @@ -178,15 +176,12 @@ fn actor_methods(actor: Option<&IDLActorType>) -> &[syntax::Binding] { None => return &[], }; - match typ { - IDLType::ServT(methods) => methods, - IDLType::ClassT(_, inner) => { - if let IDLType::ServT(methods) = inner.as_ref() { - methods - } else { - &[] - } - } + match &typ.kind { + IDLTypeKind::ServT(methods) => methods, + IDLTypeKind::ClassT(_, inner) => match &inner.kind { + IDLTypeKind::ServT(methods) => methods, + _ => &[], + }, _ => &[], } } @@ -924,22 +919,20 @@ impl<'b> NominalState<'_, 'b> { let res = match t.as_ref() { TypeInner::Opt(ty) => { path.push(TypePath::Opt); - let syntax_ty = if let Some(IDLType::OptT(inner)) = syntax { - Some(inner.as_ref()) - } else { - None - }; + let syntax_ty = syntax.and_then(|s| match &s.kind { + IDLTypeKind::OptT(inner) => Some(inner.as_ref()), + _ => None, + }); let ty = self.nominalize(env, path, ty, syntax_ty); path.pop(); TypeInner::Opt(ty) } TypeInner::Vec(ty) => { path.push(TypePath::Vec); - let syntax_ty = if let Some(IDLType::VecT(inner)) = syntax { - Some(inner.as_ref()) - } else { - None - }; + let syntax_ty = syntax.and_then(|s| match &s.kind { + IDLTypeKind::VecT(inner) => Some(inner.as_ref()), + _ => None, + }); let ty = self.nominalize(env, path, ty, syntax_ty); path.pop(); TypeInner::Vec(ty) @@ -1129,11 +1122,10 @@ impl<'b> NominalState<'_, 'b> { } }, TypeInner::Class(args, ty) => { - let syntax_ty = if let Some(IDLType::ClassT(_, syntax_ty)) = syntax { - Some(syntax_ty.as_ref()) - } else { - None - }; + let syntax_ty = syntax.and_then(|s| match &s.kind { + IDLTypeKind::ClassT(_, syntax_ty) => Some(syntax_ty.as_ref()), + _ => None, + }); TypeInner::Class( args.iter() .map(|arg| { diff --git a/rust/candid_parser/src/bindings/typescript.rs b/rust/candid_parser/src/bindings/typescript.rs index ce09e3754..19e2c2df6 100644 --- a/rust/candid_parser/src/bindings/typescript.rs +++ b/rust/candid_parser/src/bindings/typescript.rs @@ -1,5 +1,5 @@ use super::javascript::{ident, is_tuple_fields}; -use crate::syntax::{self, IDLMergedProg, IDLType}; +use crate::syntax::{self, IDLMergedProg, IDLType, IDLTypeKind}; use candid::pretty::utils::*; use candid::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; use pretty::RcDoc; @@ -29,23 +29,48 @@ fn pp_ty_rich<'a>( syntax: Option<&'a IDLType>, is_ref: bool, ) -> RcDoc<'a> { - match (ty.as_ref(), syntax) { - (TypeInner::Record(ref fields), Some(IDLType::RecordT(syntax_fields))) => { - pp_record(env, fields, Some(syntax_fields), is_ref) + match ty.as_ref() { + TypeInner::Record(ref fields) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::RecordT(syntax_fields) = &syntax_ty.kind { + return pp_record(env, fields, Some(syntax_fields), is_ref); + } + } + pp_record(env, fields, None, is_ref) } - (TypeInner::Variant(ref fields), Some(IDLType::VariantT(syntax_fields))) => { - pp_variant(env, fields, Some(syntax_fields), is_ref) + TypeInner::Variant(ref fields) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::VariantT(syntax_fields) = &syntax_ty.kind { + return pp_variant(env, fields, Some(syntax_fields), is_ref); + } + } + pp_variant(env, fields, None, is_ref) } - (TypeInner::Service(ref serv), Some(IDLType::ServT(syntax_serv))) => { - pp_service(env, serv, Some(syntax_serv)) + TypeInner::Service(ref serv) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::ServT(syntax_serv) = &syntax_ty.kind { + return pp_service(env, serv, Some(syntax_serv)); + } + } + pp_service(env, serv, None) } - (TypeInner::Opt(ref t), Some(IDLType::OptT(syntax_inner))) => { - pp_opt(env, t, Some(syntax_inner), is_ref) + TypeInner::Opt(ref t) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::OptT(syntax_inner) = &syntax_ty.kind { + return pp_opt(env, t, Some(syntax_inner), is_ref); + } + } + pp_opt(env, t, None, is_ref) } - (TypeInner::Vec(ref t), Some(IDLType::VecT(syntax_inner))) => { - pp_vec(env, t, Some(syntax_inner), is_ref) + TypeInner::Vec(ref t) => { + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::VecT(syntax_inner) = &syntax_ty.kind { + return pp_vec(env, t, Some(syntax_inner), is_ref); + } + } + pp_vec(env, t, None, is_ref) } - (_, _) => pp_ty(env, ty, is_ref), + _ => pp_ty(env, ty, is_ref), } } @@ -308,11 +333,12 @@ fn pp_actor<'a>(env: &'a TypeEnv, ty: &'a Type, syntax: Option<&'a IDLType>) -> .append(str(id)) .append(str(" {}")), TypeInner::Class(_, t) => { - if let Some(IDLType::ClassT(_, syntax_t)) = syntax { - pp_actor(env, t, Some(syntax_t)) - } else { - pp_actor(env, t, None) + if let Some(syntax_ty) = syntax { + if let IDLTypeKind::ClassT(_, syntax_t) = &syntax_ty.kind { + return pp_actor(env, t, Some(syntax_t)); + } } + pp_actor(env, t, None) } _ => unreachable!(), } diff --git a/rust/candid_parser/src/grammar.lalrpop b/rust/candid_parser/src/grammar.lalrpop index 584be1729..71cec6962 100644 --- a/rust/candid_parser/src/grammar.lalrpop +++ b/rust/candid_parser/src/grammar.lalrpop @@ -1,7 +1,7 @@ use super::test::{Assert, Input, Test}; use super::token::{Token, error2, LexicalError, Span, TriviaMap}; use candid::{Principal, types::Label}; -use crate::syntax::{IDLType, PrimType, TypeField, FuncType, Binding, Dec, IDLProg, IDLTypes, IDLInitArgs, IDLActorType}; +use crate::syntax::{Binding, Dec, FuncType, IDLActorType, IDLInitArgs, IDLProg, IDLType, IDLTypeKind, IDLTypes, PrimType, TypeField}; use candid::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; use candid::types::{TypeEnv, FuncMode}; use candid::utils::check_unique; @@ -163,58 +163,101 @@ pub Typs: IDLTypes = TupTyp => IDLTypes { args:<> }; pub Typ: IDLType = { PrimTyp => <>, - "opt" => IDLType::OptT(Box::new(<>)), - "vec" => IDLType::VecT(Box::new(<>)), - "blob" => IDLType::VecT(Box::new(IDLType::PrimT(PrimType::Nat8))), - "record" "{" >> "}" =>? { + "opt" => IDLType::new(l..r, IDLTypeKind::OptT(Box::new(typ))), + "vec" => IDLType::new(l..r, IDLTypeKind::VecT(Box::new(typ))), + "blob" => { + let span = l..r; + let elem = IDLType::new(span.clone(), IDLTypeKind::PrimT(PrimType::Nat8)); + IDLType::new(span, IDLTypeKind::VecT(Box::new(elem))) + }, + "record" "{" >> "}" =>? { let mut id: u32 = 0; - let span = <>.1.clone(); - let mut fs: Vec = <>.0.iter().map(|f| { - let label = match f.label { - Label::Unnamed(_) => { id = id + 1; Label::Unnamed(id - 1) }, - ref l => { id = l.get_id() + 1; l.clone() }, - }; - TypeField { label, typ: f.typ.clone(), docs: f.docs.clone() } + let span = l..r; + let inner_span = fields.1.clone(); + let mut fs: Vec = fields.0.iter().map(|f| { + let label = match &f.label { + Label::Unnamed(_) => { + id += 1; + Label::Unnamed(id - 1) + } + ref l => { + id = l.get_id() + 1; + (*l).clone() + } + }; + TypeField { + label, + typ: f.typ.clone(), + docs: f.docs.clone(), + span: f.span.clone(), + } }).collect(); fs.sort_unstable_by_key(|TypeField { label, .. }| label.get_id()); - check_unique(fs.iter().map(|f| &f.label)).map_err(|e| error2(e, span))?; - Ok(IDLType::RecordT(fs)) + check_unique(fs.iter().map(|f| &f.label)).map_err(|e| error2(e, inner_span))?; + Ok(IDLType::new(span, IDLTypeKind::RecordT(fs))) }, - "variant" "{" >> "}" =>? { - let span = fs.1.clone(); - fs.0.sort_unstable_by_key(|TypeField { label, .. }| label.get_id()); - check_unique(fs.0.iter().map(|f| &f.label)).map_err(|e| error2(e, span))?; - Ok(IDLType::VariantT(fs.0)) + "variant" "{" >> "}" =>? { + let span = l..r; + let inner_span = fields.1.clone(); + fields.0.sort_unstable_by_key(|TypeField { label, .. }| label.get_id()); + check_unique(fields.0.iter().map(|f| &f.label)).map_err(|e| error2(e, inner_span))?; + Ok(IDLType::new(span, IDLTypeKind::VariantT(fields.0))) }, - "func" => IDLType::FuncT(<>), - "service" => IDLType::ServT(<>), - "principal" => IDLType::PrincipalT, + "func" => IDLType::new(l..r, IDLTypeKind::FuncT(typ)), + "service" => IDLType::new(l..r, IDLTypeKind::ServT(typ)), + "principal" => IDLType::new(l..r, IDLTypeKind::PrincipalT), } PrimTyp: IDLType = { - "null" => IDLType::PrimT(PrimType::Null), - "id" => { - match PrimType::str_to_enum(&<>) { - Some(p) => IDLType::PrimT(p), - None => IDLType::VarT(<>), - } + "null" => IDLType::new(l..r, IDLTypeKind::PrimT(PrimType::Null)), + => { + let span = l..r; + match PrimType::str_to_enum(&ident) { + Some(p) => IDLType::new(span, IDLTypeKind::PrimT(p)), + None => IDLType::new(span, IDLTypeKind::VarT(ident)), + } }, } FieldTyp: TypeField = { - ":" =>? Ok(TypeField { label: Label::Id(id), typ, docs: doc_comment.unwrap_or_default() }), - ":" => TypeField { label: Label::Named(n), typ, docs: doc_comment.unwrap_or_default() }, + ":" =>? Ok(TypeField { + label: Label::Id(id), + typ, + docs: doc_comment.unwrap_or_default(), + span: l..r, + }), + ":" => TypeField { + label: Label::Named(n), + typ, + docs: doc_comment.unwrap_or_default(), + span: l..r, + }, } RecordFieldTyp: TypeField = { FieldTyp => <>, - => TypeField { label: Label::Unnamed(0), typ, docs: doc_comment.unwrap_or_default() }, + => TypeField { + label: Label::Unnamed(0), + typ, + docs: doc_comment.unwrap_or_default(), + span: l..r, + }, } VariantFieldTyp: TypeField = { FieldTyp => <>, - => TypeField { label: Label::Named(n), typ: IDLType::PrimT(PrimType::Null), docs: doc_comment.unwrap_or_default() }, - =>? Ok(TypeField { label: Label::Id(id), typ: IDLType::PrimT(PrimType::Null), docs: doc_comment.unwrap_or_default() }), + => TypeField { + label: Label::Named(n), + typ: IDLType::new(l..r, IDLTypeKind::PrimT(PrimType::Null)), + docs: doc_comment.unwrap_or_default(), + span: l..r, + }, + =>? Ok(TypeField { + label: Label::Id(id), + typ: IDLType::new(l..r, IDLTypeKind::PrimT(PrimType::Null)), + docs: doc_comment.unwrap_or_default(), + span: l..r, + }), } TupTyp: Vec = "(" > ")" => <>; @@ -245,33 +288,56 @@ ActorTyp: Vec = { } MethTyp: Binding = { - ":" => Binding { id: n, typ: IDLType::FuncT(f), docs: doc_comment.unwrap_or_default() }, - ":" => Binding { id: n, typ: IDLType::VarT(id), docs: doc_comment.unwrap_or_default() }, + ":" => Binding { + id: n, + typ: IDLType::new(l..r, IDLTypeKind::FuncT(f)), + docs: doc_comment.unwrap_or_default(), + span: l..r, + }, + ":" => Binding { + id: n, + typ: IDLType::new(l..r, IDLTypeKind::VarT(id)), + docs: doc_comment.unwrap_or_default(), + span: l..r, + }, } // Type declarations Def: Dec = { - "type" "=" => Dec::TypD(Binding { id: id, typ: t, docs: doc_comment.unwrap_or_default() }), - "import" => Dec::ImportType(<>), - "import" "service" => Dec::ImportServ(<>), + "type" "=" => Dec::TypD(Binding { + id: id, + typ: t, + docs: doc_comment.unwrap_or_default(), + span: l..r, + }), + "import" => Dec::ImportType { path: text, span: l..r }, + "import" "service" => Dec::ImportServ { path: text, span: l..r }, } Actor: IDLType = { - ActorTyp => IDLType::ServT(<>), - "id" => IDLType::VarT(<>), + => IDLType::new(l..r, IDLTypeKind::ServT(typ)), + => IDLType::new(l..r, IDLTypeKind::VarT(name)), } MainActor: IDLActorType = { - "service" "id"? ":" ";"? => IDLActorType { typ: t, docs: doc_comment.unwrap_or_default() }, - "service" "id"? ":" "->" ";"? => IDLActorType { typ: IDLType::ClassT(args, Box::new(t)), docs: doc_comment.unwrap_or_default() }, + "service" "id"? ":" ";"? => IDLActorType { + typ: t, + docs: doc_comment.unwrap_or_default(), + span: l..r, + }, + "service" "id"? ":" "->" ";"? => IDLActorType { + typ: IDLType::new(l..r, IDLTypeKind::ClassT(args, Box::new(t))), + docs: doc_comment.unwrap_or_default(), + span: l..r, + }, } pub IDLProg: IDLProg = { - > => IDLProg { decs, actor } + > => IDLProg { decs, actor, span: l..r } } pub IDLInitArgs: IDLInitArgs = { - > => IDLInitArgs { decs, args } + > => IDLInitArgs { decs, args, span: l..r } } // Test file. Follows the "specification" in test/README.md diff --git a/rust/candid_parser/src/syntax/mod.rs b/rust/candid_parser/src/syntax/mod.rs index 8a9c51f33..3e6dd55f4 100644 --- a/rust/candid_parser/src/syntax/mod.rs +++ b/rust/candid_parser/src/syntax/mod.rs @@ -2,7 +2,7 @@ mod pretty; pub use pretty::pretty_print; -use crate::error; +use crate::{error, token::Span}; use anyhow::{anyhow, bail, Context, Result}; use candid::{ idl_hash, @@ -11,33 +11,41 @@ use candid::{ use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq)] -pub enum IDLType { - PrimT(PrimType), - VarT(String), - FuncT(FuncType), - OptT(Box), - VecT(Box), - RecordT(Vec), - VariantT(Vec), - ServT(Vec), - ClassT(Vec, Box), - PrincipalT, +pub struct Spanned { + pub value: T, + pub span: Span, +} + +impl Spanned { + pub fn new(value: T, span: Span) -> Self { + Spanned { value, span } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IDLType { + pub span: Span, + pub kind: IDLTypeKind, } impl IDLType { + pub fn new(span: Span, kind: IDLTypeKind) -> Self { + IDLType { span, kind } + } + pub fn is_tuple(&self) -> bool { - match self { - IDLType::RecordT(ref fs) => { - for (i, field) in fs.iter().enumerate() { - if field.label.get_id() != (i as u32) { - return false; - } - } - true - } + match &self.kind { + IDLTypeKind::RecordT(ref fs) => fs + .iter() + .enumerate() + .all(|(i, field)| field.label.get_id() == (i as u32)), _ => false, } } + + pub fn synthetic(kind: IDLTypeKind) -> Self { + IDLType { span: 0..0, kind } + } } impl std::str::FromStr for IDLType { @@ -49,6 +57,20 @@ impl std::str::FromStr for IDLType { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IDLTypeKind { + PrimT(PrimType), + VarT(String), + FuncT(FuncType), + OptT(Box), + VecT(Box), + RecordT(Vec), + VariantT(Vec), + ServT(Vec), + ClassT(Vec, Box), + PrincipalT, +} + #[derive(Debug, Clone)] pub struct IDLTypes { pub args: Vec, @@ -115,13 +137,14 @@ pub struct TypeField { pub label: Label, pub typ: IDLType, pub docs: Vec, + pub span: Span, } #[derive(Debug)] pub enum Dec { TypD(Binding), - ImportType(String), - ImportServ(String), + ImportType { path: String, span: Span }, + ImportServ { path: String, span: Span }, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -129,18 +152,21 @@ pub struct Binding { pub id: String, pub typ: IDLType, pub docs: Vec, + pub span: Span, } #[derive(Debug, Clone)] pub struct IDLActorType { pub typ: IDLType, pub docs: Vec, + pub span: Span, } #[derive(Debug)] pub struct IDLProg { pub decs: Vec, pub actor: Option, + pub span: Span, } impl IDLProg { @@ -168,6 +194,7 @@ impl std::str::FromStr for IDLProg { pub struct IDLInitArgs { pub decs: Vec, pub args: Vec, + pub span: Span, } impl std::str::FromStr for IDLInitArgs { @@ -219,32 +246,35 @@ impl IDLMergedProg { } pub fn resolve_actor(&self) -> Result> { - let (init_args, top_level_docs, mut methods) = match &self.main_actor { + let mut init_args: Option> = None; + let mut top_level_docs: Vec = vec![]; + let mut actor_span: Span = 0..0; + let mut methods = match &self.main_actor { None => { if self.service_imports.is_empty() { return Ok(None); - } else { - (None, vec![], vec![]) + } + vec![] + } + Some(actor) if self.service_imports.is_empty() => return Ok(Some(actor.clone())), + Some(actor) => { + top_level_docs = actor.docs.clone(); + actor_span = actor.span.clone(); + match &actor.typ.kind { + IDLTypeKind::ClassT(args, inner) => { + init_args = Some(args.clone()); + self.chase_service((**inner).clone(), None)? + } + _ => self.chase_service(actor.typ.clone(), None)?, } } - Some(t) if self.service_imports.is_empty() => return Ok(Some(t.clone())), - Some(IDLActorType { - typ: IDLType::ClassT(args, inner), - docs, - }) => ( - Some(args.clone()), - docs.clone(), - self.chase_service(*inner.clone(), None)?, - ), - Some(ty) => ( - None, - ty.docs.clone(), - self.chase_service(ty.typ.clone(), None)?, - ), }; for (name, typ) in &self.service_imports { methods.extend(self.chase_service(typ.typ.clone(), Some(name))?); + if top_level_docs.is_empty() { + top_level_docs = typ.docs.clone(); + } } let mut hashes: HashMap = HashMap::new(); @@ -255,21 +285,24 @@ impl IDLMergedProg { } } + let service_type = IDLType::synthetic(IDLTypeKind::ServT(methods)); let typ = if let Some(args) = init_args { - IDLType::ClassT(args, Box::new(IDLType::ServT(methods))) + IDLType::synthetic(IDLTypeKind::ClassT(args, Box::new(service_type.clone()))) } else { - IDLType::ServT(methods) + service_type }; + Ok(Some(IDLActorType { typ, docs: top_level_docs, + span: actor_span, })) } // NOTE: We don't worry about cyclic type definitions, as we rule those out earlier when checking the type decs fn chase_service(&self, ty: IDLType, import_name: Option<&str>) -> Result> { - match ty { - IDLType::VarT(v) => { + match ty.kind { + IDLTypeKind::VarT(v) => { let resolved = self .typ_decs .iter() @@ -277,10 +310,10 @@ impl IDLMergedProg { .with_context(|| format!("Unbound type identifier {v}"))?; self.chase_service(resolved.typ.clone(), import_name) } - IDLType::ServT(bindings) => Ok(bindings), - ty => Err(import_name + IDLTypeKind::ServT(bindings) => Ok(bindings), + ty_kind => Err(import_name .map(|name| anyhow!("Imported service file \"{name}\" has a service constructor")) - .unwrap_or(anyhow!("not a service type: {:?}", ty))), + .unwrap_or(anyhow!("not a service type: {:?}", ty_kind))), } } } diff --git a/rust/candid_parser/src/syntax/pretty.rs b/rust/candid_parser/src/syntax/pretty.rs index 1b1b1970e..e0d225acb 100644 --- a/rust/candid_parser/src/syntax/pretty.rs +++ b/rust/candid_parser/src/syntax/pretty.rs @@ -5,44 +5,45 @@ use crate::{ candid::{pp_docs, pp_label_raw, pp_modes, pp_text}, utils::{concat, enclose, enclose_space, ident, kwd, lines, str, INDENT_SPACE, LINE_WIDTH}, }, - syntax::{Binding, FuncType, IDLActorType, IDLMergedProg, IDLType, PrimType, TypeField}, + syntax::{ + Binding, FuncType, IDLActorType, IDLMergedProg, IDLType, IDLTypeKind, PrimType, TypeField, + }, }; fn pp_ty(ty: &IDLType) -> RcDoc<'_> { - use IDLType::*; - match ty { - PrimT(PrimType::Null) => str("null"), - PrimT(PrimType::Bool) => str("bool"), - PrimT(PrimType::Nat) => str("nat"), - PrimT(PrimType::Int) => str("int"), - PrimT(PrimType::Nat8) => str("nat8"), - PrimT(PrimType::Nat16) => str("nat16"), - PrimT(PrimType::Nat32) => str("nat32"), - PrimT(PrimType::Nat64) => str("nat64"), - PrimT(PrimType::Int8) => str("int8"), - PrimT(PrimType::Int16) => str("int16"), - PrimT(PrimType::Int32) => str("int32"), - PrimT(PrimType::Int64) => str("int64"), - PrimT(PrimType::Float32) => str("float32"), - PrimT(PrimType::Float64) => str("float64"), - PrimT(PrimType::Text) => str("text"), - PrimT(PrimType::Reserved) => str("reserved"), - PrimT(PrimType::Empty) => str("empty"), - VarT(ref s) => str(s), - PrincipalT => str("principal"), - OptT(ref t) => pp_opt(t), - VecT(ref t) => pp_vec(t), - RecordT(ref fs) => pp_record(fs, ty.is_tuple()), - VariantT(ref fs) => pp_variant(fs), - FuncT(ref func) => pp_function(func), - ServT(ref serv) => pp_service(serv), - ClassT(ref args, ref t) => pp_class(args, t), + match &ty.kind { + IDLTypeKind::PrimT(PrimType::Null) => str("null"), + IDLTypeKind::PrimT(PrimType::Bool) => str("bool"), + IDLTypeKind::PrimT(PrimType::Nat) => str("nat"), + IDLTypeKind::PrimT(PrimType::Int) => str("int"), + IDLTypeKind::PrimT(PrimType::Nat8) => str("nat8"), + IDLTypeKind::PrimT(PrimType::Nat16) => str("nat16"), + IDLTypeKind::PrimT(PrimType::Nat32) => str("nat32"), + IDLTypeKind::PrimT(PrimType::Nat64) => str("nat64"), + IDLTypeKind::PrimT(PrimType::Int8) => str("int8"), + IDLTypeKind::PrimT(PrimType::Int16) => str("int16"), + IDLTypeKind::PrimT(PrimType::Int32) => str("int32"), + IDLTypeKind::PrimT(PrimType::Int64) => str("int64"), + IDLTypeKind::PrimT(PrimType::Float32) => str("float32"), + IDLTypeKind::PrimT(PrimType::Float64) => str("float64"), + IDLTypeKind::PrimT(PrimType::Text) => str("text"), + IDLTypeKind::PrimT(PrimType::Reserved) => str("reserved"), + IDLTypeKind::PrimT(PrimType::Empty) => str("empty"), + IDLTypeKind::VarT(ref s) => str(s), + IDLTypeKind::PrincipalT => str("principal"), + IDLTypeKind::OptT(ref t) => pp_opt(t), + IDLTypeKind::VecT(ref t) => pp_vec(t), + IDLTypeKind::RecordT(ref fs) => pp_record(fs, ty.is_tuple()), + IDLTypeKind::VariantT(ref fs) => pp_variant(fs), + IDLTypeKind::FuncT(ref func) => pp_function(func), + IDLTypeKind::ServT(ref serv) => pp_service(serv), + IDLTypeKind::ClassT(ref args, ref t) => pp_class(args, t), } } fn pp_field(field: &TypeField, is_variant: bool) -> RcDoc<'_> { let docs = pp_docs(&field.docs); - let ty_doc = if is_variant && field.typ == IDLType::PrimT(PrimType::Null) { + let ty_doc = if is_variant && matches!(field.typ.kind, IDLTypeKind::PrimT(PrimType::Null)) { RcDoc::nil() } else { kwd(" :").append(pp_ty(&field.typ)) @@ -60,7 +61,7 @@ fn pp_opt(ty: &IDLType) -> RcDoc<'_> { } fn pp_vec(ty: &IDLType) -> RcDoc<'_> { - if matches!(ty, IDLType::PrimT(PrimType::Nat8)) { + if matches!(ty.kind, IDLTypeKind::PrimT(PrimType::Nat8)) { str("blob") } else { kwd("vec").append(pp_ty(ty)) @@ -110,9 +111,9 @@ fn pp_service(methods: &[Binding]) -> RcDoc<'_> { fn pp_service_methods(methods: &[Binding]) -> RcDoc<'_> { let methods = methods.iter().map(|b| { let docs = pp_docs(&b.docs); - let func_doc = match b.typ { - IDLType::FuncT(ref f) => pp_method(f), - IDLType::VarT(_) => pp_ty(&b.typ), + let func_doc = match &b.typ.kind { + IDLTypeKind::FuncT(ref f) => pp_method(f), + IDLTypeKind::VarT(_) => pp_ty(&b.typ), _ => unreachable!(), }; docs.append(pp_text(&b.id)) @@ -125,9 +126,9 @@ fn pp_service_methods(methods: &[Binding]) -> RcDoc<'_> { fn pp_class<'a>(args: &'a [IDLType], t: &'a IDLType) -> RcDoc<'a> { let doc = pp_args(args).append(" ->").append(RcDoc::space()); - match t { - IDLType::ServT(ref serv) => doc.append(pp_service_methods(serv)), - IDLType::VarT(ref s) => doc.append(s), + match &t.kind { + IDLTypeKind::ServT(ref serv) => doc.append(pp_service_methods(serv)), + IDLTypeKind::VarT(ref s) => doc.append(s), _ => unreachable!(), } } @@ -145,9 +146,9 @@ fn pp_defs(prog: &IDLMergedProg) -> RcDoc<'_> { fn pp_actor(actor: &IDLActorType) -> RcDoc<'_> { let docs = pp_docs(&actor.docs); - let service_doc = match actor.typ { - IDLType::ServT(ref serv) => pp_service_methods(serv), - IDLType::VarT(_) | IDLType::ClassT(_, _) => pp_ty(&actor.typ), + let service_doc = match &actor.typ.kind { + IDLTypeKind::ServT(ref serv) => pp_service_methods(serv), + IDLTypeKind::VarT(_) | IDLTypeKind::ClassT(_, _) => pp_ty(&actor.typ), _ => unreachable!(), }; docs.append(kwd("service :")).append(service_doc) diff --git a/rust/candid_parser/src/test.rs b/rust/candid_parser/src/test.rs index 10735e941..ef07f02ea 100644 --- a/rust/candid_parser/src/test.rs +++ b/rust/candid_parser/src/test.rs @@ -151,6 +151,7 @@ pub fn check(test: Test) -> Result<()> { let prog = IDLProg { decs: test.defs, actor: None, + span: 0..0, }; check_prog(&mut env, &prog)?; let mut count = 0; diff --git a/rust/candid_parser/src/typing.rs b/rust/candid_parser/src/typing.rs index f661dc8ea..ddf8a1622 100644 --- a/rust/candid_parser/src/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -1,8 +1,8 @@ use crate::{ pretty_parse, syntax::{ - Binding, Dec, IDLActorType, IDLInitArgs, IDLMergedProg, IDLProg, IDLType, PrimType, - TypeField, + Binding, Dec, IDLActorType, IDLInitArgs, IDLMergedProg, IDLProg, IDLType, IDLTypeKind, + PrimType, TypeField, }, Error, Result, }; @@ -48,30 +48,30 @@ fn check_prim(prim: &PrimType) -> Type { } pub fn check_type(env: &Env, t: &IDLType) -> Result { - match t { - IDLType::PrimT(prim) => Ok(check_prim(prim)), - IDLType::VarT(id) => { + match &t.kind { + IDLTypeKind::PrimT(prim) => Ok(check_prim(prim)), + IDLTypeKind::VarT(id) => { env.te.find_type(id)?; Ok(TypeInner::Var(id.to_string()).into()) } - IDLType::OptT(t) => { + IDLTypeKind::OptT(t) => { let t = check_type(env, t)?; Ok(TypeInner::Opt(t).into()) } - IDLType::VecT(t) => { + IDLTypeKind::VecT(t) => { let t = check_type(env, t)?; Ok(TypeInner::Vec(t).into()) } - IDLType::RecordT(fs) => { + IDLTypeKind::RecordT(fs) => { let fs = check_fields(env, fs)?; Ok(TypeInner::Record(fs).into()) } - IDLType::VariantT(fs) => { + IDLTypeKind::VariantT(fs) => { let fs = check_fields(env, fs)?; Ok(TypeInner::Variant(fs).into()) } - IDLType::PrincipalT => Ok(TypeInner::Principal.into()), - IDLType::FuncT(func) => { + IDLTypeKind::PrincipalT => Ok(TypeInner::Principal.into()), + IDLTypeKind::FuncT(func) => { let mut t1 = Vec::new(); for arg in func.args.iter() { t1.push(check_type(env, arg)?); @@ -96,11 +96,11 @@ pub fn check_type(env: &Env, t: &IDLType) -> Result { }; Ok(TypeInner::Func(f).into()) } - IDLType::ServT(ms) => { + IDLTypeKind::ServT(ms) => { let ms = check_meths(env, ms)?; Ok(TypeInner::Service(ms).into()) } - IDLType::ClassT(_, _) => Err(Error::msg("service constructor not supported")), + IDLTypeKind::ClassT(_, _) => Err(Error::msg("service constructor not supported")), } } @@ -137,11 +137,16 @@ fn check_meths(env: &Env, ms: &[Binding]) -> Result> { fn check_defs(env: &mut Env, decs: &[Dec]) -> Result<()> { for dec in decs.iter() { match dec { - Dec::TypD(Binding { id, typ, docs: _ }) => { + Dec::TypD(Binding { + id, + typ, + docs: _, + span: _, + }) => { let t = check_type(env, typ)?; env.te.0.insert(id.to_string(), t); } - Dec::ImportType(_) | Dec::ImportServ(_) => (), + Dec::ImportType { .. } | Dec::ImportServ { .. } => (), } } Ok(()) @@ -190,7 +195,10 @@ fn check_decs(env: &mut Env, decs: &[Dec]) -> Result<()> { fn check_actor(env: &Env, actor: &Option) -> Result> { match actor.as_ref().map(|a| &a.typ) { None => Ok(None), - Some(IDLType::ClassT(ts, t)) => { + Some(IDLType { + kind: IDLTypeKind::ClassT(ts, t), + .. + }) => { let mut args = Vec::new(); for arg in ts.iter() { args.push(check_type(env, arg)?); @@ -225,8 +233,8 @@ fn load_imports( list: &mut Vec<(PathBuf, String, IDLProg)>, ) -> Result<()> { for dec in prog.decs.iter() { - let include_serv = matches!(dec, Dec::ImportServ(_)); - if let Dec::ImportType(file) | Dec::ImportServ(file) = dec { + if let Dec::ImportType { path: file, .. } | Dec::ImportServ { path: file, .. } = dec { + let include_serv = matches!(dec, Dec::ImportServ { .. }); let path = resolve_path(base, file); match visited.get_mut(&path) { Some(x) => *x = *x || include_serv, diff --git a/rust/candid_parser/tests/parse_type.rs b/rust/candid_parser/tests/parse_type.rs index 4bf218ce7..1c2176694 100644 --- a/rust/candid_parser/tests/parse_type.rs +++ b/rust/candid_parser/tests/parse_type.rs @@ -2,7 +2,7 @@ use candid::types::TypeEnv; use candid_parser::{ bindings::{javascript, motoko, rust, typescript}, configs::Configs, - syntax::{pretty_print, Dec, IDLProg, IDLType}, + syntax::{pretty_print, Dec, IDLProg, IDLTypeKind}, typing::{check_file, check_prog}, }; use goldenfile::Mint; @@ -42,7 +42,9 @@ service server : { let actor = ast.actor.unwrap(); assert_eq!(actor.docs, vec!["Doc comment for service"]); - let IDLType::ServT(methods) = &actor.typ else { + let methods = if let IDLTypeKind::ServT(methods) = &actor.typ.kind { + methods + } else { panic!("actor is not a service"); }; assert_eq!(methods[0].docs, vec!["Doc comment for f"]); @@ -82,9 +84,11 @@ service server : { } }) .unwrap(); - match &list.typ { - IDLType::OptT(list_inner) => { - let IDLType::RecordT(fields) = list_inner.as_ref() else { + match &list.typ.kind { + IDLTypeKind::OptT(list_inner) => { + let fields = if let IDLTypeKind::RecordT(fields) = &list_inner.kind { + fields + } else { panic!("inner is not a record"); }; assert_eq!(fields[0].docs, vec!["Doc comment for List.head"]); diff --git a/rust/candid_parser/tests/test_doc_comments.rs b/rust/candid_parser/tests/test_doc_comments.rs index 53762f80b..d8e3a2f42 100644 --- a/rust/candid_parser/tests/test_doc_comments.rs +++ b/rust/candid_parser/tests/test_doc_comments.rs @@ -1,4 +1,4 @@ -use candid_parser::syntax::{Binding, Dec, IDLProg, IDLType, TypeField}; +use candid_parser::syntax::{Binding, Dec, IDLProg, IDLType, IDLTypeKind, TypeField}; #[test] fn test_doc_comments_separated_by_blank_line() { @@ -162,7 +162,7 @@ fn extract_type_declaration(dec: &Dec) -> &Binding { } fn extract_variant_fields(typ: &IDLType) -> &[TypeField] { - if let IDLType::VariantT(fields) = typ { + if let IDLTypeKind::VariantT(fields) = &typ.kind { return fields; } panic!("Expected a variant type"); From 4757cdecbf2c085bf7b3dd23f672786cd8e33f85 Mon Sep 17 00:00:00 2001 From: Yota <91780796+wiyota@users.noreply.github.com> Date: Mon, 10 Nov 2025 06:57:52 +0900 Subject: [PATCH 2/4] feat(parser): add error recovery and token streaming APIs Add lossy parsing capabilities to recover valid declarations from partially-invalid Candid programs, along with API for parsing from custom token iterators. - Add IDLProgLossy parser rule with RecoverDef and MainActorLossy recovery points to collect errors while parsing valid declarations - Add parse_prog_lossy() public API returning partial AST and error list - Add parse_idl_prog_from_tokens() for custom token iterator support - Make ParserError public to enable error handling in external code - Reorganize token imports for consistency (alphabetical ordering) - Add comprehensive tests for both new parsing modes --- rust/candid_parser/src/grammar.lalrpop | 43 +++++++++++++++- rust/candid_parser/src/lib.rs | 25 +++++++++ rust/candid_parser/src/syntax/mod.rs | 30 ++++++++++- rust/candid_parser/src/token.rs | 2 +- rust/candid_parser/tests/parse_prog.rs | 70 ++++++++++++++++++++++++++ 5 files changed, 166 insertions(+), 4 deletions(-) create mode 100644 rust/candid_parser/tests/parse_prog.rs diff --git a/rust/candid_parser/src/grammar.lalrpop b/rust/candid_parser/src/grammar.lalrpop index 71cec6962..1a5c1662b 100644 --- a/rust/candid_parser/src/grammar.lalrpop +++ b/rust/candid_parser/src/grammar.lalrpop @@ -1,5 +1,5 @@ use super::test::{Assert, Input, Test}; -use super::token::{Token, error2, LexicalError, Span, TriviaMap}; +use super::token::{error2, LexicalError, ParserError, Span, Token, TriviaMap}; use candid::{Principal, types::Label}; use crate::syntax::{Binding, Dec, FuncType, IDLActorType, IDLInitArgs, IDLProg, IDLType, IDLTypeKind, IDLTypes, PrimType, TypeField}; use candid::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; @@ -314,6 +314,11 @@ Def: Dec = { "import" "service" => Dec::ImportServ { path: text, span: l..r }, } +RecoverDef: Result = { + Def => Ok(<>), + => Err(err.error), +} + Actor: IDLType = { => IDLType::new(l..r, IDLTypeKind::ServT(typ)), => IDLType::new(l..r, IDLTypeKind::VarT(name)), @@ -332,10 +337,46 @@ MainActor: IDLActorType = { }, } +MainActorLossy: Result = { + MainActor => Ok(<>), + <_doc_comment: DocComment> "service" "id"? ":" ";"? => Err(err.error), + <_doc_comment: DocComment> "service" "id"? ":" <_args:TupTyp> "->" ";"? => Err(err.error), + <_doc_comment: DocComment> "service" => Err(err.error), +} + pub IDLProg: IDLProg = { > => IDLProg { decs, actor, span: l..r } } +pub IDLProgLossy: (Option, Vec) = { + > => { + let mut errors = Vec::new(); + let mut ok_decs = Vec::new(); + for dec in decs { + match dec { + Ok(dec) => ok_decs.push(dec), + Err(err) => errors.push(err), + } + } + let actor = match actor { + None => None, + Some(result) => match result { + Ok(actor) => Some(actor), + Err(err) => { + errors.push(err); + None + } + }, + }; + let prog = if ok_decs.is_empty() && actor.is_none() { + None + } else { + Some(IDLProg { decs: ok_decs, actor, span: l..r }) + }; + (prog, errors) + } +} + pub IDLInitArgs: IDLInitArgs = { > => IDLInitArgs { decs, args, span: l..r } } diff --git a/rust/candid_parser/src/lib.rs b/rust/candid_parser/src/lib.rs index 63d3674b4..ec90ed651 100644 --- a/rust/candid_parser/src/lib.rs +++ b/rust/candid_parser/src/lib.rs @@ -151,3 +151,28 @@ pub fn parse_idl_value(s: &str) -> crate::Result { let lexer = token::Tokenizer::new(s); Ok(grammar::ArgParser::new().parse(None, lexer)?) } + +/// Parse an `IDLProg` from any iterator over lexer tokens. +/// +/// This allows callers to provide their own iterator that may, for example, +/// record every consumed token before it is fed to the parser. +pub fn parse_idl_prog_from_tokens( + trivia: Option<&token::TriviaMap>, + tokens: I, +) -> std::result::Result +where + I: IntoIterator>, +{ + IDLProg::parse_from_tokens(trivia, tokens) +} + +/// Parse a `.did` program while attempting to recover from syntax errors. +/// +/// The returned vector contains every `lalrpop_util::ParseError` that was reported +/// during parsing. Lexer errors bubble up as `ParseError::User { .. }`, which +/// lets callers distinguish them from structural syntax issues. +pub fn parse_prog_lossy(s: &str) -> (Option, Vec) { + let trivia = token::TriviaMap::default(); + let lexer = token::Tokenizer::new_with_trivia(s, trivia.clone()); + IDLProg::parse_lossy_from_tokens(Some(&trivia), lexer) +} diff --git a/rust/candid_parser/src/syntax/mod.rs b/rust/candid_parser/src/syntax/mod.rs index 3e6dd55f4..263f07623 100644 --- a/rust/candid_parser/src/syntax/mod.rs +++ b/rust/candid_parser/src/syntax/mod.rs @@ -2,7 +2,10 @@ mod pretty; pub use pretty::pretty_print; -use crate::{error, token::Span}; +use crate::{ + error, + token::{LexicalError, ParserError, Span, Token, TriviaMap}, +}; use anyhow::{anyhow, bail, Context, Result}; use candid::{ idl_hash, @@ -179,6 +182,29 @@ impl IDLProg { } }) } + + pub fn parse_from_tokens( + trivia: Option<&TriviaMap>, + tokens: I, + ) -> std::result::Result + where + I: IntoIterator>, + { + super::grammar::IDLProgParser::new().parse(trivia, tokens) + } + + pub fn parse_lossy_from_tokens( + trivia: Option<&TriviaMap>, + tokens: I, + ) -> (Option, Vec) + where + I: IntoIterator>, + { + match super::grammar::IDLProgLossyParser::new().parse(trivia, tokens) { + Ok(result) => result, + Err(err) => (None, vec![err]), + } + } } impl std::str::FromStr for IDLProg { @@ -186,7 +212,7 @@ impl std::str::FromStr for IDLProg { fn from_str(str: &str) -> error::Result { let trivia = super::token::TriviaMap::default(); let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(super::grammar::IDLProgParser::new().parse(Some(&trivia), lexer)?) + Ok(Self::parse_from_tokens(Some(&trivia), lexer)?) } } diff --git a/rust/candid_parser/src/token.rs b/rust/candid_parser/src/token.rs index 37ea787fa..b48c7cb47 100644 --- a/rust/candid_parser/src/token.rs +++ b/rust/candid_parser/src/token.rs @@ -223,7 +223,7 @@ impl LexicalError { } } -pub(crate) type ParserError = ParseError; +pub type ParserError = ParseError; pub fn error(err: E) -> ParserError { ParseError::User { error: LexicalError::new(err, 0..0), diff --git a/rust/candid_parser/tests/parse_prog.rs b/rust/candid_parser/tests/parse_prog.rs new file mode 100644 index 000000000..f61bda8a6 --- /dev/null +++ b/rust/candid_parser/tests/parse_prog.rs @@ -0,0 +1,70 @@ +use candid_parser::{ + parse_idl_prog_from_tokens, parse_prog_lossy, + syntax::Dec, + token::{self, TriviaMap}, +}; + +fn decl_names(decs: &[Dec]) -> Vec<&str> { + decs.iter() + .filter_map(|dec| match dec { + Dec::TypD(binding) => Some(binding.id.as_str()), + _ => None, + }) + .collect() +} + +#[test] +fn parses_program_from_recorded_tokens() { + let source = r#" + // doc: Foo + type Foo = record { field : int }; + service : { foo : () -> (); }; + "#; + let trivia = TriviaMap::default(); + let tokens: Vec<_> = token::Tokenizer::new_with_trivia(source, trivia.clone()).collect(); + let prog = parse_idl_prog_from_tokens(Some(&trivia), tokens).expect("parses"); + + let names = decl_names(&prog.decs); + assert_eq!(names, vec!["Foo"]); + let docs = match &prog.decs[0] { + Dec::TypD(binding) => binding.docs.clone(), + _ => unreachable!(), + }; + assert_eq!(docs, vec!["doc: Foo".to_string()]); + assert!( + prog.actor.is_some(), + "service definition should be preserved" + ); +} + +#[test] +fn lossy_parser_recovers_declarations_and_reports_errors() { + let source = r#" + type Good = record { foo : int }; + type Broken = record { foo : }; + type AlsoGood = record { bar : text }; + service : { + bad : ( -> ) -> (); + ok : () -> (); + }; + "#; + + let (maybe_prog, errors) = parse_prog_lossy(source); + assert!( + errors.len() >= 2, + "should surface multiple parse errors, got {errors:?}" + ); + assert!( + errors + .iter() + .any(|err| matches!(err, lalrpop_util::ParseError::UnrecognizedToken { .. })), + "expected at least one syntax error in the error list" + ); + let prog = maybe_prog.expect("should recover valid declarations"); + let names = decl_names(&prog.decs); + assert_eq!(names, vec!["Good", "AlsoGood"]); + assert!( + prog.actor.is_none(), + "actor should be dropped due to unrecoverable service parse error" + ); +} From 7e38599de1eb8d2f812d40a4c7560a4d77b76f62 Mon Sep 17 00:00:00 2001 From: Yota <91780796+wiyota@users.noreply.github.com> Date: Mon, 10 Nov 2025 08:08:35 +0900 Subject: [PATCH 3/4] refactor(syntax): separate spanned and spanless AST types Split AST into spanned and spanless representations to allow different use cases to choose the appropriate level of detail. - Create syntax::spanned module with all original Span-aware types - Add spanless types in syntax root for consumers that don't need spans - Implement From conversions for compatibility - Simplify pattern matching in code generators by matching directly on IDLType enum variants instead of extracting .kind field - Update grammar to produce spanned types, convert to spanless at API boundaries - Update all tests to work with new type structure --- rust/candid_parser/src/bindings/motoko.rs | 84 +++-- rust/candid_parser/src/bindings/rust.rs | 14 +- rust/candid_parser/src/bindings/typescript.rs | 36 +-- rust/candid_parser/src/grammar.lalrpop | 31 +- rust/candid_parser/src/syntax/mod.rs | 248 ++++++++------ rust/candid_parser/src/syntax/pretty.rs | 12 +- rust/candid_parser/src/syntax/spanned.rs | 302 ++++++++++++++++++ rust/candid_parser/src/test.rs | 1 - rust/candid_parser/src/typing.rs | 28 +- rust/candid_parser/tests/parse_type.rs | 6 +- rust/candid_parser/tests/test_doc_comments.rs | 2 +- 11 files changed, 558 insertions(+), 206 deletions(-) create mode 100644 rust/candid_parser/src/syntax/spanned.rs diff --git a/rust/candid_parser/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs index 0c7d94197..601080ae6 100644 --- a/rust/candid_parser/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -99,50 +99,38 @@ fn escape(id: &str, is_method: bool) -> RcDoc<'_> { fn pp_ty_rich<'a>(ty: &'a Type, syntax: Option<&'a IDLType>) -> RcDoc<'a> { match ty.as_ref() { TypeInner::Service(ref meths) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::ServT(methods) = &syntax_ty.kind { - return pp_service(meths, Some(methods)); - } + if let Some(IDLTypeKind::ServT(methods)) = syntax { + return pp_service(meths, Some(methods)); } pp_service(meths, None) } TypeInner::Class(ref args, t) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::ClassT(_, syntax_t) = &syntax_ty.kind { - return pp_class((args, t), Some(syntax_t)); - } + if let Some(IDLTypeKind::ClassT(_, syntax_t)) = syntax { + return pp_class((args, t), Some(syntax_t)); } pp_class((args, t), None) } TypeInner::Record(ref fields) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::RecordT(syntax_fields) = &syntax_ty.kind { - return pp_record(fields, Some(syntax_fields)); - } + if let Some(IDLTypeKind::RecordT(syntax_fields)) = syntax { + return pp_record(fields, Some(syntax_fields)); } pp_record(fields, None) } TypeInner::Variant(ref fields) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::VariantT(syntax_fields) = &syntax_ty.kind { - return pp_variant(fields, Some(syntax_fields)); - } + if let Some(IDLTypeKind::VariantT(syntax_fields)) = syntax { + return pp_variant(fields, Some(syntax_fields)); } pp_variant(fields, None) } TypeInner::Opt(ref inner) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::OptT(syntax_inner) = &syntax_ty.kind { - return str("?").append(pp_ty_rich(inner, Some(syntax_inner))); - } + if let Some(IDLTypeKind::OptT(syntax_inner)) = syntax { + return str("?").append(pp_ty_rich(inner, Some(syntax_inner))); } str("?").append(pp_ty(inner)) } TypeInner::Vec(ref inner) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::VecT(syntax_inner) = &syntax_ty.kind { - return pp_vec(inner, Some(syntax_inner)); - } + if let Some(IDLTypeKind::VecT(syntax_inner)) = syntax { + return pp_vec(inner, Some(syntax_inner)); } pp_vec(inner, None) } @@ -342,27 +330,33 @@ fn pp_defs<'a>(env: &'a TypeEnv, prog: &'a IDLMergedProg) -> RcDoc<'a> { fn pp_actor<'a>(ty: &'a Type, syntax: Option<&'a IDLActorType>) -> RcDoc<'a> { let self_doc = kwd("public type Self ="); match ty.as_ref() { - TypeInner::Service(ref serv) => match syntax { - Some(IDLActorType { typ, docs, .. }) => match &typ.kind { - IDLTypeKind::ServT(fields) => { - let docs = pp_docs(docs); - docs.append(self_doc).append(pp_service(serv, Some(fields))) - } - _ => pp_service(serv, None), - }, - None => pp_service(serv, None), - }, - TypeInner::Class(ref args, ref t) => match syntax { - Some(IDLActorType { typ, docs, .. }) => match &typ.kind { - IDLTypeKind::ClassT(_, syntax_t) => { - let docs = pp_docs(docs); - docs.append(self_doc) - .append(pp_class((args, t), Some(syntax_t))) - } - _ => self_doc.append(pp_class((args, t), None)), - }, - None => self_doc.append(pp_class((args, t), None)), - }, + TypeInner::Service(ref serv) => { + if let Some(IDLActorType { + typ: IDLTypeKind::ServT(fields), + docs, + .. + }) = syntax + { + let docs = pp_docs(docs); + docs.append(self_doc).append(pp_service(serv, Some(fields))) + } else { + pp_service(serv, None) + } + } + TypeInner::Class(ref args, ref t) => { + if let Some(IDLActorType { + typ: IDLTypeKind::ClassT(_, syntax_t), + docs, + .. + }) = syntax + { + let docs = pp_docs(docs); + docs.append(self_doc) + .append(pp_class((args, t), Some(syntax_t))) + } else { + self_doc.append(pp_class((args, t), None)) + } + } TypeInner::Var(_) => self_doc.append(pp_ty(ty)), _ => unreachable!(), } diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index b5b8c0af9..33c8a1d45 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -157,14 +157,14 @@ fn find_field<'a>( } fn record_syntax_fields(syntax: Option<&IDLType>) -> Option<&[syntax::TypeField]> { - syntax.and_then(|ty| match &ty.kind { + syntax.and_then(|ty| match ty { IDLTypeKind::RecordT(fields) => Some(fields.as_slice()), _ => None, }) } fn variant_syntax_fields(syntax: Option<&IDLType>) -> Option<&[syntax::TypeField]> { - syntax.and_then(|ty| match &ty.kind { + syntax.and_then(|ty| match ty { IDLTypeKind::VariantT(fields) => Some(fields.as_slice()), _ => None, }) @@ -176,9 +176,9 @@ fn actor_methods(actor: Option<&IDLActorType>) -> &[syntax::Binding] { None => return &[], }; - match &typ.kind { + match typ { IDLTypeKind::ServT(methods) => methods, - IDLTypeKind::ClassT(_, inner) => match &inner.kind { + IDLTypeKind::ClassT(_, inner) => match inner.as_ref() { IDLTypeKind::ServT(methods) => methods, _ => &[], }, @@ -919,7 +919,7 @@ impl<'b> NominalState<'_, 'b> { let res = match t.as_ref() { TypeInner::Opt(ty) => { path.push(TypePath::Opt); - let syntax_ty = syntax.and_then(|s| match &s.kind { + let syntax_ty = syntax.and_then(|s| match s { IDLTypeKind::OptT(inner) => Some(inner.as_ref()), _ => None, }); @@ -929,7 +929,7 @@ impl<'b> NominalState<'_, 'b> { } TypeInner::Vec(ty) => { path.push(TypePath::Vec); - let syntax_ty = syntax.and_then(|s| match &s.kind { + let syntax_ty = syntax.and_then(|s| match s { IDLTypeKind::VecT(inner) => Some(inner.as_ref()), _ => None, }); @@ -1122,7 +1122,7 @@ impl<'b> NominalState<'_, 'b> { } }, TypeInner::Class(args, ty) => { - let syntax_ty = syntax.and_then(|s| match &s.kind { + let syntax_ty = syntax.and_then(|s| match s { IDLTypeKind::ClassT(_, syntax_ty) => Some(syntax_ty.as_ref()), _ => None, }); diff --git a/rust/candid_parser/src/bindings/typescript.rs b/rust/candid_parser/src/bindings/typescript.rs index 19e2c2df6..ad6c03a5e 100644 --- a/rust/candid_parser/src/bindings/typescript.rs +++ b/rust/candid_parser/src/bindings/typescript.rs @@ -31,42 +31,32 @@ fn pp_ty_rich<'a>( ) -> RcDoc<'a> { match ty.as_ref() { TypeInner::Record(ref fields) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::RecordT(syntax_fields) = &syntax_ty.kind { - return pp_record(env, fields, Some(syntax_fields), is_ref); - } + if let Some(IDLTypeKind::RecordT(syntax_fields)) = syntax { + return pp_record(env, fields, Some(syntax_fields), is_ref); } pp_record(env, fields, None, is_ref) } TypeInner::Variant(ref fields) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::VariantT(syntax_fields) = &syntax_ty.kind { - return pp_variant(env, fields, Some(syntax_fields), is_ref); - } + if let Some(IDLTypeKind::VariantT(syntax_fields)) = syntax { + return pp_variant(env, fields, Some(syntax_fields), is_ref); } pp_variant(env, fields, None, is_ref) } TypeInner::Service(ref serv) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::ServT(syntax_serv) = &syntax_ty.kind { - return pp_service(env, serv, Some(syntax_serv)); - } + if let Some(IDLTypeKind::ServT(syntax_serv)) = syntax { + return pp_service(env, serv, Some(syntax_serv)); } pp_service(env, serv, None) } TypeInner::Opt(ref t) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::OptT(syntax_inner) = &syntax_ty.kind { - return pp_opt(env, t, Some(syntax_inner), is_ref); - } + if let Some(IDLTypeKind::OptT(syntax_inner)) = syntax { + return pp_opt(env, t, Some(syntax_inner), is_ref); } pp_opt(env, t, None, is_ref) } TypeInner::Vec(ref t) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::VecT(syntax_inner) = &syntax_ty.kind { - return pp_vec(env, t, Some(syntax_inner), is_ref); - } + if let Some(IDLTypeKind::VecT(syntax_inner)) = syntax { + return pp_vec(env, t, Some(syntax_inner), is_ref); } pp_vec(env, t, None, is_ref) } @@ -333,10 +323,8 @@ fn pp_actor<'a>(env: &'a TypeEnv, ty: &'a Type, syntax: Option<&'a IDLType>) -> .append(str(id)) .append(str(" {}")), TypeInner::Class(_, t) => { - if let Some(syntax_ty) = syntax { - if let IDLTypeKind::ClassT(_, syntax_t) = &syntax_ty.kind { - return pp_actor(env, t, Some(syntax_t)); - } + if let Some(IDLTypeKind::ClassT(_, syntax_t)) = syntax { + return pp_actor(env, t, Some(syntax_t)); } pp_actor(env, t, None) } diff --git a/rust/candid_parser/src/grammar.lalrpop b/rust/candid_parser/src/grammar.lalrpop index 1a5c1662b..2b0849cd4 100644 --- a/rust/candid_parser/src/grammar.lalrpop +++ b/rust/candid_parser/src/grammar.lalrpop @@ -1,7 +1,8 @@ use super::test::{Assert, Input, Test}; use super::token::{error2, LexicalError, ParserError, Span, Token, TriviaMap}; use candid::{Principal, types::Label}; -use crate::syntax::{Binding, Dec, FuncType, IDLActorType, IDLInitArgs, IDLProg, IDLType, IDLTypeKind, IDLTypes, PrimType, TypeField}; +use crate::syntax::spanned::{Binding, Dec, FuncType, IDLActorType, IDLInitArgs, IDLProg, IDLType, IDLTypeKind, IDLTypes, TypeField}; +use crate::syntax::PrimType; use candid::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; use candid::types::{TypeEnv, FuncMode}; use candid::utils::check_unique; @@ -113,7 +114,8 @@ AnnVal: IDLValue = { => <>, > ":" > =>? { let env = TypeEnv::new(); - let typ = crate::typing::ast_to_type(&env, &typ.0).map_err(|e| error2(e, typ.1))?; + let spanless: crate::syntax::IDLType = typ.0.clone().into(); + let typ = crate::typing::ast_to_type(&env, &spanless).map_err(|e| error2(e, typ.1))?; arg.0.annotate_type(true, &env, &typ).map_err(|e| error2(e, arg.1)) } } @@ -395,14 +397,29 @@ Assert: Assert = > =>? { }; Assertion: Assert = { - ":" => Assert { left, right: None, typ, pass: true, desc }, - "!:" => Assert { left, right: None, typ, pass: false, desc }, - "==" ":" => Assert { left, right: Some(right), typ, pass: true, desc }, - "!=" ":" => Assert { left, right: Some(right), typ, pass: false, desc }, + ":" => { + let typ: Vec = typ.into_iter().map(Into::into).collect(); + Assert { left, right: None, typ, pass: true, desc } + }, + "!:" => { + let typ: Vec = typ.into_iter().map(Into::into).collect(); + Assert { left, right: None, typ, pass: false, desc } + }, + "==" ":" => { + let typ: Vec = typ.into_iter().map(Into::into).collect(); + Assert { left, right: Some(right), typ, pass: true, desc } + }, + "!=" ":" => { + let typ: Vec = typ.into_iter().map(Into::into).collect(); + Assert { left, right: Some(right), typ, pass: false, desc } + }, } pub Test: Test = { - > > => Test { defs, asserts }, + > > => { + let defs = defs.into_iter().map(Into::into).collect(); + Test { defs, asserts } + }, } // Common util diff --git a/rust/candid_parser/src/syntax/mod.rs b/rust/candid_parser/src/syntax/mod.rs index 263f07623..337b5dee1 100644 --- a/rust/candid_parser/src/syntax/mod.rs +++ b/rust/candid_parser/src/syntax/mod.rs @@ -1,10 +1,11 @@ mod pretty; +pub mod spanned; pub use pretty::pretty_print; use crate::{ error, - token::{LexicalError, ParserError, Span, Token, TriviaMap}, + token::{LexicalError, ParserError, Token, TriviaMap}, }; use anyhow::{anyhow, bail, Context, Result}; use candid::{ @@ -14,64 +15,68 @@ use candid::{ use std::collections::HashMap; #[derive(Debug, Clone, PartialEq, Eq)] -pub struct Spanned { - pub value: T, - pub span: Span, -} - -impl Spanned { - pub fn new(value: T, span: Span) -> Self { - Spanned { value, span } - } +pub enum IDLType { + PrimT(PrimType), + VarT(String), + FuncT(FuncType), + OptT(Box), + VecT(Box), + RecordT(Vec), + VariantT(Vec), + ServT(Vec), + ClassT(Vec, Box), + PrincipalT, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IDLType { - pub span: Span, - pub kind: IDLTypeKind, -} +pub type IDLTypeKind = IDLType; impl IDLType { - pub fn new(span: Span, kind: IDLTypeKind) -> Self { - IDLType { span, kind } - } - pub fn is_tuple(&self) -> bool { - match &self.kind { - IDLTypeKind::RecordT(ref fs) => fs - .iter() - .enumerate() - .all(|(i, field)| field.label.get_id() == (i as u32)), + match self { + IDLType::RecordT(ref fs) => { + for (i, field) in fs.iter().enumerate() { + if field.label.get_id() != (i as u32) { + return false; + } + } + true + } _ => false, } } - - pub fn synthetic(kind: IDLTypeKind) -> Self { - IDLType { span: 0..0, kind } - } } impl std::str::FromStr for IDLType { type Err = error::Error; fn from_str(str: &str) -> error::Result { - let trivia = super::token::TriviaMap::default(); - let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(super::grammar::TypParser::new().parse(Some(&trivia), lexer)?) + Ok(spanned::IDLType::from_str(str)?.into()) } } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IDLTypeKind { - PrimT(PrimType), - VarT(String), - FuncT(FuncType), - OptT(Box), - VecT(Box), - RecordT(Vec), - VariantT(Vec), - ServT(Vec), - ClassT(Vec, Box), - PrincipalT, +impl From for IDLType { + fn from(value: spanned::IDLType) -> Self { + match value.kind { + spanned::IDLTypeKind::PrimT(p) => IDLType::PrimT(p), + spanned::IDLTypeKind::VarT(v) => IDLType::VarT(v), + spanned::IDLTypeKind::FuncT(f) => IDLType::FuncT(f.into()), + spanned::IDLTypeKind::OptT(t) => IDLType::OptT(Box::new((*t).into())), + spanned::IDLTypeKind::VecT(t) => IDLType::VecT(Box::new((*t).into())), + spanned::IDLTypeKind::RecordT(fs) => { + IDLType::RecordT(fs.into_iter().map(Into::into).collect()) + } + spanned::IDLTypeKind::VariantT(fs) => { + IDLType::VariantT(fs.into_iter().map(Into::into).collect()) + } + spanned::IDLTypeKind::ServT(bs) => { + IDLType::ServT(bs.into_iter().map(Into::into).collect()) + } + spanned::IDLTypeKind::ClassT(args, ty) => IDLType::ClassT( + args.into_iter().map(Into::into).collect(), + Box::new((*ty).into()), + ), + spanned::IDLTypeKind::PrincipalT => IDLType::PrincipalT, + } + } } #[derive(Debug, Clone)] @@ -82,9 +87,15 @@ pub struct IDLTypes { impl std::str::FromStr for IDLTypes { type Err = error::Error; fn from_str(str: &str) -> error::Result { - let trivia = super::token::TriviaMap::default(); - let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(super::grammar::TypsParser::new().parse(Some(&trivia), lexer)?) + Ok(spanned::IDLTypes::from_str(str)?.into()) + } +} + +impl From for IDLTypes { + fn from(types: spanned::IDLTypes) -> Self { + IDLTypes { + args: types.args.into_iter().map(Into::into).collect(), + } } } @@ -135,19 +146,48 @@ pub struct FuncType { pub rets: Vec, } +impl From for FuncType { + fn from(func: spanned::FuncType) -> Self { + FuncType { + modes: func.modes, + args: func.args.into_iter().map(Into::into).collect(), + rets: func.rets.into_iter().map(Into::into).collect(), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypeField { pub label: Label, pub typ: IDLType, pub docs: Vec, - pub span: Span, +} + +impl From for TypeField { + fn from(field: spanned::TypeField) -> Self { + TypeField { + label: field.label, + typ: field.typ.into(), + docs: field.docs, + } + } } #[derive(Debug)] pub enum Dec { TypD(Binding), - ImportType { path: String, span: Span }, - ImportServ { path: String, span: Span }, + ImportType(String), + ImportServ(String), +} + +impl From for Dec { + fn from(dec: spanned::Dec) -> Self { + match dec { + spanned::Dec::TypD(binding) => Dec::TypD(binding.into()), + spanned::Dec::ImportType { path, .. } => Dec::ImportType(path), + spanned::Dec::ImportServ { path, .. } => Dec::ImportServ(path), + } + } } #[derive(Debug, Clone, PartialEq, Eq)] @@ -155,21 +195,37 @@ pub struct Binding { pub id: String, pub typ: IDLType, pub docs: Vec, - pub span: Span, +} + +impl From for Binding { + fn from(binding: spanned::Binding) -> Self { + Binding { + id: binding.id, + typ: binding.typ.into(), + docs: binding.docs, + } + } } #[derive(Debug, Clone)] pub struct IDLActorType { pub typ: IDLType, pub docs: Vec, - pub span: Span, +} + +impl From for IDLActorType { + fn from(actor: spanned::IDLActorType) -> Self { + IDLActorType { + typ: actor.typ.into(), + docs: actor.docs, + } + } } #[derive(Debug)] pub struct IDLProg { pub decs: Vec, pub actor: Option, - pub span: Span, } impl IDLProg { @@ -190,7 +246,7 @@ impl IDLProg { where I: IntoIterator>, { - super::grammar::IDLProgParser::new().parse(trivia, tokens) + spanned::IDLProg::parse_from_tokens(trivia, tokens).map(Into::into) } pub fn parse_lossy_from_tokens( @@ -200,19 +256,24 @@ impl IDLProg { where I: IntoIterator>, { - match super::grammar::IDLProgLossyParser::new().parse(trivia, tokens) { - Ok(result) => result, - Err(err) => (None, vec![err]), - } + let (res, errors) = spanned::IDLProg::parse_lossy_from_tokens(trivia, tokens); + (res.map(Into::into), errors) } } impl std::str::FromStr for IDLProg { type Err = error::Error; fn from_str(str: &str) -> error::Result { - let trivia = super::token::TriviaMap::default(); - let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(Self::parse_from_tokens(Some(&trivia), lexer)?) + Ok(spanned::IDLProg::from_str(str)?.into()) + } +} + +impl From for IDLProg { + fn from(prog: spanned::IDLProg) -> Self { + IDLProg { + decs: prog.decs.into_iter().map(Into::into).collect(), + actor: prog.actor.map(Into::into), + } } } @@ -220,15 +281,20 @@ impl std::str::FromStr for IDLProg { pub struct IDLInitArgs { pub decs: Vec, pub args: Vec, - pub span: Span, } impl std::str::FromStr for IDLInitArgs { type Err = error::Error; fn from_str(str: &str) -> error::Result { - let trivia = super::token::TriviaMap::default(); - let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(super::grammar::IDLInitArgsParser::new().parse(Some(&trivia), lexer)?) + Ok(spanned::IDLInitArgs::from_str(str)?.into()) + } +} +impl From for IDLInitArgs { + fn from(args: spanned::IDLInitArgs) -> Self { + IDLInitArgs { + decs: args.decs.into_iter().map(Into::into).collect(), + args: args.args.into_iter().map(Into::into).collect(), + } } } @@ -272,35 +338,32 @@ impl IDLMergedProg { } pub fn resolve_actor(&self) -> Result> { - let mut init_args: Option> = None; - let mut top_level_docs: Vec = vec![]; - let mut actor_span: Span = 0..0; - let mut methods = match &self.main_actor { + let (init_args, top_level_docs, mut methods) = match &self.main_actor { None => { if self.service_imports.is_empty() { return Ok(None); - } - vec![] - } - Some(actor) if self.service_imports.is_empty() => return Ok(Some(actor.clone())), - Some(actor) => { - top_level_docs = actor.docs.clone(); - actor_span = actor.span.clone(); - match &actor.typ.kind { - IDLTypeKind::ClassT(args, inner) => { - init_args = Some(args.clone()); - self.chase_service((**inner).clone(), None)? - } - _ => self.chase_service(actor.typ.clone(), None)?, + } else { + (None, vec![], vec![]) } } + Some(t) if self.service_imports.is_empty() => return Ok(Some(t.clone())), + Some(IDLActorType { + typ: IDLType::ClassT(args, inner), + docs, + }) => ( + Some(args.clone()), + docs.clone(), + self.chase_service(*inner.clone(), None)?, + ), + Some(ty) => ( + None, + ty.docs.clone(), + self.chase_service(ty.typ.clone(), None)?, + ), }; for (name, typ) in &self.service_imports { methods.extend(self.chase_service(typ.typ.clone(), Some(name))?); - if top_level_docs.is_empty() { - top_level_docs = typ.docs.clone(); - } } let mut hashes: HashMap = HashMap::new(); @@ -311,24 +374,21 @@ impl IDLMergedProg { } } - let service_type = IDLType::synthetic(IDLTypeKind::ServT(methods)); let typ = if let Some(args) = init_args { - IDLType::synthetic(IDLTypeKind::ClassT(args, Box::new(service_type.clone()))) + IDLType::ClassT(args, Box::new(IDLType::ServT(methods))) } else { - service_type + IDLType::ServT(methods) }; - Ok(Some(IDLActorType { typ, docs: top_level_docs, - span: actor_span, })) } // NOTE: We don't worry about cyclic type definitions, as we rule those out earlier when checking the type decs fn chase_service(&self, ty: IDLType, import_name: Option<&str>) -> Result> { - match ty.kind { - IDLTypeKind::VarT(v) => { + match ty { + IDLType::VarT(v) => { let resolved = self .typ_decs .iter() @@ -336,10 +396,10 @@ impl IDLMergedProg { .with_context(|| format!("Unbound type identifier {v}"))?; self.chase_service(resolved.typ.clone(), import_name) } - IDLTypeKind::ServT(bindings) => Ok(bindings), - ty_kind => Err(import_name + IDLType::ServT(bindings) => Ok(bindings), + ty => Err(import_name .map(|name| anyhow!("Imported service file \"{name}\" has a service constructor")) - .unwrap_or(anyhow!("not a service type: {:?}", ty_kind))), + .unwrap_or(anyhow!("not a service type: {:?}", ty))), } } } diff --git a/rust/candid_parser/src/syntax/pretty.rs b/rust/candid_parser/src/syntax/pretty.rs index e0d225acb..9d57f2393 100644 --- a/rust/candid_parser/src/syntax/pretty.rs +++ b/rust/candid_parser/src/syntax/pretty.rs @@ -11,7 +11,7 @@ use crate::{ }; fn pp_ty(ty: &IDLType) -> RcDoc<'_> { - match &ty.kind { + match ty { IDLTypeKind::PrimT(PrimType::Null) => str("null"), IDLTypeKind::PrimT(PrimType::Bool) => str("bool"), IDLTypeKind::PrimT(PrimType::Nat) => str("nat"), @@ -43,7 +43,7 @@ fn pp_ty(ty: &IDLType) -> RcDoc<'_> { fn pp_field(field: &TypeField, is_variant: bool) -> RcDoc<'_> { let docs = pp_docs(&field.docs); - let ty_doc = if is_variant && matches!(field.typ.kind, IDLTypeKind::PrimT(PrimType::Null)) { + let ty_doc = if is_variant && matches!(field.typ, IDLTypeKind::PrimT(PrimType::Null)) { RcDoc::nil() } else { kwd(" :").append(pp_ty(&field.typ)) @@ -61,7 +61,7 @@ fn pp_opt(ty: &IDLType) -> RcDoc<'_> { } fn pp_vec(ty: &IDLType) -> RcDoc<'_> { - if matches!(ty.kind, IDLTypeKind::PrimT(PrimType::Nat8)) { + if matches!(ty, IDLTypeKind::PrimT(PrimType::Nat8)) { str("blob") } else { kwd("vec").append(pp_ty(ty)) @@ -111,7 +111,7 @@ fn pp_service(methods: &[Binding]) -> RcDoc<'_> { fn pp_service_methods(methods: &[Binding]) -> RcDoc<'_> { let methods = methods.iter().map(|b| { let docs = pp_docs(&b.docs); - let func_doc = match &b.typ.kind { + let func_doc = match &b.typ { IDLTypeKind::FuncT(ref f) => pp_method(f), IDLTypeKind::VarT(_) => pp_ty(&b.typ), _ => unreachable!(), @@ -126,7 +126,7 @@ fn pp_service_methods(methods: &[Binding]) -> RcDoc<'_> { fn pp_class<'a>(args: &'a [IDLType], t: &'a IDLType) -> RcDoc<'a> { let doc = pp_args(args).append(" ->").append(RcDoc::space()); - match &t.kind { + match t { IDLTypeKind::ServT(ref serv) => doc.append(pp_service_methods(serv)), IDLTypeKind::VarT(ref s) => doc.append(s), _ => unreachable!(), @@ -146,7 +146,7 @@ fn pp_defs(prog: &IDLMergedProg) -> RcDoc<'_> { fn pp_actor(actor: &IDLActorType) -> RcDoc<'_> { let docs = pp_docs(&actor.docs); - let service_doc = match &actor.typ.kind { + let service_doc = match &actor.typ { IDLTypeKind::ServT(ref serv) => pp_service_methods(serv), IDLTypeKind::VarT(_) | IDLTypeKind::ClassT(_, _) => pp_ty(&actor.typ), _ => unreachable!(), diff --git a/rust/candid_parser/src/syntax/spanned.rs b/rust/candid_parser/src/syntax/spanned.rs new file mode 100644 index 000000000..bad48cea8 --- /dev/null +++ b/rust/candid_parser/src/syntax/spanned.rs @@ -0,0 +1,302 @@ +use super::PrimType; +use crate::{ + error, + token::{LexicalError, ParserError, Span, Token, TriviaMap}, +}; +use anyhow::{anyhow, bail, Context, Result}; +use candid::{ + idl_hash, + types::{FuncMode, Label}, +}; +use std::collections::HashMap; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Spanned { + pub value: T, + pub span: Span, +} + +impl Spanned { + pub fn new(value: T, span: Span) -> Self { + Spanned { value, span } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IDLType { + pub span: Span, + pub kind: IDLTypeKind, +} + +impl IDLType { + pub fn new(span: Span, kind: IDLTypeKind) -> Self { + IDLType { span, kind } + } + + pub fn is_tuple(&self) -> bool { + match &self.kind { + IDLTypeKind::RecordT(ref fs) => fs + .iter() + .enumerate() + .all(|(i, field)| field.label.get_id() == (i as u32)), + _ => false, + } + } + + pub fn synthetic(kind: IDLTypeKind) -> Self { + IDLType { span: 0..0, kind } + } +} + +impl std::str::FromStr for IDLType { + type Err = error::Error; + fn from_str(str: &str) -> error::Result { + let trivia = crate::token::TriviaMap::default(); + let lexer = crate::token::Tokenizer::new_with_trivia(str, trivia.clone()); + Ok(crate::grammar::TypParser::new().parse(Some(&trivia), lexer)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IDLTypeKind { + PrimT(PrimType), + VarT(String), + FuncT(FuncType), + OptT(Box), + VecT(Box), + RecordT(Vec), + VariantT(Vec), + ServT(Vec), + ClassT(Vec, Box), + PrincipalT, +} + +#[derive(Debug, Clone)] +pub struct IDLTypes { + pub args: Vec, +} + +impl std::str::FromStr for IDLTypes { + type Err = error::Error; + fn from_str(str: &str) -> error::Result { + let trivia = crate::token::TriviaMap::default(); + let lexer = crate::token::Tokenizer::new_with_trivia(str, trivia.clone()); + Ok(crate::grammar::TypsParser::new().parse(Some(&trivia), lexer)?) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FuncType { + pub modes: Vec, + pub args: Vec, + pub rets: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct TypeField { + pub label: Label, + pub typ: IDLType, + pub docs: Vec, + pub span: Span, +} + +#[derive(Debug)] +pub enum Dec { + TypD(Binding), + ImportType { path: String, span: Span }, + ImportServ { path: String, span: Span }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Binding { + pub id: String, + pub typ: IDLType, + pub docs: Vec, + pub span: Span, +} + +#[derive(Debug, Clone)] +pub struct IDLActorType { + pub typ: IDLType, + pub docs: Vec, + pub span: Span, +} + +#[derive(Debug)] +pub struct IDLProg { + pub decs: Vec, + pub actor: Option, + pub span: Span, +} + +impl IDLProg { + pub fn typ_decs(decs: Vec) -> impl Iterator { + decs.into_iter().filter_map(|d| { + if let Dec::TypD(bindings) = d { + Some(bindings) + } else { + None + } + }) + } + + pub fn parse_from_tokens( + trivia: Option<&TriviaMap>, + tokens: I, + ) -> std::result::Result + where + I: IntoIterator>, + { + crate::grammar::IDLProgParser::new().parse(trivia, tokens) + } + + pub fn parse_lossy_from_tokens( + trivia: Option<&TriviaMap>, + tokens: I, + ) -> (Option, Vec) + where + I: IntoIterator>, + { + match crate::grammar::IDLProgLossyParser::new().parse(trivia, tokens) { + Ok(result) => result, + Err(err) => (None, vec![err]), + } + } +} + +impl std::str::FromStr for IDLProg { + type Err = error::Error; + fn from_str(str: &str) -> error::Result { + let trivia = crate::token::TriviaMap::default(); + let lexer = crate::token::Tokenizer::new_with_trivia(str, trivia.clone()); + Ok(Self::parse_from_tokens(Some(&trivia), lexer)?) + } +} + +#[derive(Debug)] +pub struct IDLInitArgs { + pub decs: Vec, + pub args: Vec, + pub span: Span, +} + +impl std::str::FromStr for IDLInitArgs { + type Err = error::Error; + fn from_str(str: &str) -> error::Result { + let trivia = crate::token::TriviaMap::default(); + let lexer = crate::token::Tokenizer::new_with_trivia(str, trivia.clone()); + Ok(crate::grammar::IDLInitArgsParser::new().parse(Some(&trivia), lexer)?) + } +} + +#[derive(Debug)] +pub struct IDLMergedProg { + typ_decs: Vec, + main_actor: Option, + service_imports: Vec<(String, IDLActorType)>, +} + +impl IDLMergedProg { + pub fn new(prog: IDLProg) -> IDLMergedProg { + IDLMergedProg { + typ_decs: IDLProg::typ_decs(prog.decs).collect(), + main_actor: prog.actor, + service_imports: vec![], + } + } + + pub fn merge(&mut self, is_service_import: bool, name: String, prog: IDLProg) -> Result<()> { + self.typ_decs.extend(IDLProg::typ_decs(prog.decs)); + if is_service_import { + let actor = prog + .actor + .with_context(|| format!("Imported service file \"{name}\" has no main service"))?; + self.service_imports.push((name, actor)); + } + Ok(()) + } + + pub fn lookup(&self, id: &str) -> Option<&Binding> { + self.typ_decs.iter().find(|b| b.id == id) + } + + pub fn decs(&self) -> Vec { + self.typ_decs.iter().map(|b| Dec::TypD(b.clone())).collect() + } + + pub fn bindings(&self) -> impl Iterator { + self.typ_decs.iter() + } + + pub fn resolve_actor(&self) -> Result> { + let mut init_args: Option> = None; + let mut top_level_docs: Vec = vec![]; + let mut actor_span: Span = 0..0; + let mut methods = match &self.main_actor { + None => { + if self.service_imports.is_empty() { + return Ok(None); + } + vec![] + } + Some(actor) if self.service_imports.is_empty() => return Ok(Some(actor.clone())), + Some(actor) => { + top_level_docs = actor.docs.clone(); + actor_span = actor.span.clone(); + match &actor.typ.kind { + IDLTypeKind::ClassT(args, inner) => { + init_args = Some(args.clone()); + self.chase_service((**inner).clone(), None)? + } + _ => self.chase_service(actor.typ.clone(), None)?, + } + } + }; + + for (name, typ) in &self.service_imports { + methods.extend(self.chase_service(typ.typ.clone(), Some(name))?); + if top_level_docs.is_empty() { + top_level_docs = typ.docs.clone(); + } + } + + let mut hashes: HashMap = HashMap::new(); + for method in &methods { + let name = &method.id; + if let Some(previous) = hashes.insert(idl_hash(name), name) { + bail!("Duplicate imported method name: label '{name}' hash collision with '{previous}'") + } + } + + let service_type = IDLType::synthetic(IDLTypeKind::ServT(methods)); + let typ = if let Some(args) = init_args { + IDLType::synthetic(IDLTypeKind::ClassT(args, Box::new(service_type.clone()))) + } else { + service_type + }; + + Ok(Some(IDLActorType { + typ, + docs: top_level_docs, + span: actor_span, + })) + } + + // NOTE: We don't worry about cyclic type definitions, as we rule those out earlier when checking the type decs + fn chase_service(&self, ty: IDLType, import_name: Option<&str>) -> Result> { + match ty.kind { + IDLTypeKind::VarT(v) => { + let resolved = self + .typ_decs + .iter() + .find(|b| b.id == v) + .with_context(|| format!("Unbound type identifier {v}"))?; + self.chase_service(resolved.typ.clone(), import_name) + } + IDLTypeKind::ServT(bindings) => Ok(bindings), + ty_kind => Err(import_name + .map(|name| anyhow!("Imported service file \"{name}\" has a service constructor")) + .unwrap_or(anyhow!("not a service type: {:?}", ty_kind))), + } + } +} diff --git a/rust/candid_parser/src/test.rs b/rust/candid_parser/src/test.rs index ef07f02ea..10735e941 100644 --- a/rust/candid_parser/src/test.rs +++ b/rust/candid_parser/src/test.rs @@ -151,7 +151,6 @@ pub fn check(test: Test) -> Result<()> { let prog = IDLProg { decs: test.defs, actor: None, - span: 0..0, }; check_prog(&mut env, &prog)?; let mut count = 0; diff --git a/rust/candid_parser/src/typing.rs b/rust/candid_parser/src/typing.rs index ddf8a1622..85d1b4918 100644 --- a/rust/candid_parser/src/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -48,7 +48,7 @@ fn check_prim(prim: &PrimType) -> Type { } pub fn check_type(env: &Env, t: &IDLType) -> Result { - match &t.kind { + match t { IDLTypeKind::PrimT(prim) => Ok(check_prim(prim)), IDLTypeKind::VarT(id) => { env.te.find_type(id)?; @@ -136,17 +136,9 @@ fn check_meths(env: &Env, ms: &[Binding]) -> Result> { fn check_defs(env: &mut Env, decs: &[Dec]) -> Result<()> { for dec in decs.iter() { - match dec { - Dec::TypD(Binding { - id, - typ, - docs: _, - span: _, - }) => { - let t = check_type(env, typ)?; - env.te.0.insert(id.to_string(), t); - } - Dec::ImportType { .. } | Dec::ImportServ { .. } => (), + if let Dec::TypD(Binding { id, typ, .. }) = dec { + let t = check_type(env, typ)?; + env.te.0.insert(id.to_string(), t); } } Ok(()) @@ -195,10 +187,7 @@ fn check_decs(env: &mut Env, decs: &[Dec]) -> Result<()> { fn check_actor(env: &Env, actor: &Option) -> Result> { match actor.as_ref().map(|a| &a.typ) { None => Ok(None), - Some(IDLType { - kind: IDLTypeKind::ClassT(ts, t), - .. - }) => { + Some(IDLTypeKind::ClassT(ts, t)) => { let mut args = Vec::new(); for arg in ts.iter() { args.push(check_type(env, arg)?); @@ -233,8 +222,11 @@ fn load_imports( list: &mut Vec<(PathBuf, String, IDLProg)>, ) -> Result<()> { for dec in prog.decs.iter() { - if let Dec::ImportType { path: file, .. } | Dec::ImportServ { path: file, .. } = dec { - let include_serv = matches!(dec, Dec::ImportServ { .. }); + if let Some((file, include_serv)) = match dec { + Dec::ImportType(file) => Some((file, false)), + Dec::ImportServ(file) => Some((file, true)), + _ => None, + } { let path = resolve_path(base, file); match visited.get_mut(&path) { Some(x) => *x = *x || include_serv, diff --git a/rust/candid_parser/tests/parse_type.rs b/rust/candid_parser/tests/parse_type.rs index 1c2176694..c1cc83434 100644 --- a/rust/candid_parser/tests/parse_type.rs +++ b/rust/candid_parser/tests/parse_type.rs @@ -42,7 +42,7 @@ service server : { let actor = ast.actor.unwrap(); assert_eq!(actor.docs, vec!["Doc comment for service"]); - let methods = if let IDLTypeKind::ServT(methods) = &actor.typ.kind { + let methods = if let IDLTypeKind::ServT(methods) = &actor.typ { methods } else { panic!("actor is not a service"); @@ -84,9 +84,9 @@ service server : { } }) .unwrap(); - match &list.typ.kind { + match &list.typ { IDLTypeKind::OptT(list_inner) => { - let fields = if let IDLTypeKind::RecordT(fields) = &list_inner.kind { + let fields = if let IDLTypeKind::RecordT(fields) = list_inner.as_ref() { fields } else { panic!("inner is not a record"); diff --git a/rust/candid_parser/tests/test_doc_comments.rs b/rust/candid_parser/tests/test_doc_comments.rs index d8e3a2f42..c90b783bc 100644 --- a/rust/candid_parser/tests/test_doc_comments.rs +++ b/rust/candid_parser/tests/test_doc_comments.rs @@ -162,7 +162,7 @@ fn extract_type_declaration(dec: &Dec) -> &Binding { } fn extract_variant_fields(typ: &IDLType) -> &[TypeField] { - if let IDLTypeKind::VariantT(fields) = &typ.kind { + if let IDLTypeKind::VariantT(fields) = typ { return fields; } panic!("Expected a variant type"); From 3535a59cd5a5dfef30481808089f980df484f2a6 Mon Sep 17 00:00:00 2001 From: Yota <91780796+wiyota@users.noreply.github.com> Date: Sun, 16 Nov 2025 08:06:38 +0900 Subject: [PATCH 4/4] refactor(parser): removing spanned module - Remove separate `spanned.rs` module in favor of inline type definitions - Introduce `IDLTypeWithSpan` wrapper to track source spans alongside type kinds - Revert `IDLType` to direct enum definition - Move span information directly into data structures (TypeField, Binding, etc.) - Update all bindings (Motoko, Rust, TypeScript) to access `typ.kind` for type information - Simplify type parsing by eliminating conversion between spanned and spanless types --- rust/candid_parser/src/bindings/motoko.rs | 102 +++--- rust/candid_parser/src/bindings/rust.rs | 78 +++-- rust/candid_parser/src/bindings/typescript.rs | 66 ++-- rust/candid_parser/src/grammar.lalrpop | 60 ++-- rust/candid_parser/src/syntax/mod.rs | 273 ++++++++-------- rust/candid_parser/src/syntax/pretty.rs | 100 +++--- rust/candid_parser/src/syntax/spanned.rs | 302 ------------------ rust/candid_parser/src/test.rs | 7 +- rust/candid_parser/src/typing.rs | 79 +++-- rust/candid_parser/tests/parse_type.rs | 14 +- rust/candid_parser/tests/test_doc_comments.rs | 10 +- tools/didc/src/main.rs | 2 +- 12 files changed, 378 insertions(+), 715 deletions(-) delete mode 100644 rust/candid_parser/src/syntax/spanned.rs diff --git a/rust/candid_parser/src/bindings/motoko.rs b/rust/candid_parser/src/bindings/motoko.rs index 601080ae6..eafa1150d 100644 --- a/rust/candid_parser/src/bindings/motoko.rs +++ b/rust/candid_parser/src/bindings/motoko.rs @@ -1,7 +1,7 @@ // This module implements the Candid to Motoko binding as specified in // https://github.com/dfinity/motoko/blob/master/design/IDL-Motoko.md -use crate::syntax::{self, IDLActorType, IDLMergedProg, IDLType, IDLTypeKind}; +use crate::syntax::{self, IDLActorType, IDLMergedProg, IDLType}; use candid::pretty::candid::is_valid_as_id; use candid::pretty::utils::*; use candid::types::{Field, FuncMode, Function, Label, SharedLabel, Type, TypeInner}; @@ -97,44 +97,26 @@ fn escape(id: &str, is_method: bool) -> RcDoc<'_> { } fn pp_ty_rich<'a>(ty: &'a Type, syntax: Option<&'a IDLType>) -> RcDoc<'a> { - match ty.as_ref() { - TypeInner::Service(ref meths) => { - if let Some(IDLTypeKind::ServT(methods)) = syntax { - return pp_service(meths, Some(methods)); - } - pp_service(meths, None) + match (ty.as_ref(), syntax) { + (TypeInner::Service(ref meths), Some(IDLType::ServT(methods))) => { + pp_service(meths, Some(methods)) } - TypeInner::Class(ref args, t) => { - if let Some(IDLTypeKind::ClassT(_, syntax_t)) = syntax { - return pp_class((args, t), Some(syntax_t)); - } - pp_class((args, t), None) + (TypeInner::Class(ref args, t), Some(IDLType::ClassT(_, syntax_t))) => { + pp_class((args, t), Some(&syntax_t.kind)) } - TypeInner::Record(ref fields) => { - if let Some(IDLTypeKind::RecordT(syntax_fields)) = syntax { - return pp_record(fields, Some(syntax_fields)); - } - pp_record(fields, None) + (TypeInner::Record(ref fields), Some(IDLType::RecordT(syntax_fields))) => { + pp_record(fields, Some(syntax_fields)) } - TypeInner::Variant(ref fields) => { - if let Some(IDLTypeKind::VariantT(syntax_fields)) = syntax { - return pp_variant(fields, Some(syntax_fields)); - } - pp_variant(fields, None) + (TypeInner::Variant(ref fields), Some(IDLType::VariantT(syntax_fields))) => { + pp_variant(fields, Some(syntax_fields)) } - TypeInner::Opt(ref inner) => { - if let Some(IDLTypeKind::OptT(syntax_inner)) = syntax { - return str("?").append(pp_ty_rich(inner, Some(syntax_inner))); - } - str("?").append(pp_ty(inner)) + (TypeInner::Opt(ref inner), Some(IDLType::OptT(syntax))) => { + str("?").append(pp_ty_rich(inner, Some(&syntax.kind))) } - TypeInner::Vec(ref inner) => { - if let Some(IDLTypeKind::VecT(syntax_inner)) = syntax { - return pp_vec(inner, Some(syntax_inner)); - } - pp_vec(inner, None) + (TypeInner::Vec(ref inner), Some(IDLType::VecT(syntax))) => { + pp_vec(inner, Some(&syntax.kind)) } - _ => pp_ty(ty), + (_, _) => pp_ty(ty), } } @@ -232,7 +214,7 @@ fn pp_service<'a>(serv: &'a [(String, Type)], syntax: Option<&'a [syntax::Bindin if let Some(bs) = syntax { if let Some(b) = bs.iter().find(|b| &b.id == id) { docs = pp_docs(&b.docs); - syntax_field_ty = Some(&b.typ) + syntax_field_ty = Some(&b.typ.kind) } } docs.append(escape(id, true)) @@ -264,7 +246,7 @@ fn find_field<'a>( if let Some(bs) = fields { if let Some(field) = bs.iter().find(|b| b.label == *label) { docs = pp_docs(&field.docs); - syntax_field_ty = Some(&field.typ); + syntax_field_ty = Some(&field.typ.kind); } }; (docs, syntax_field_ty) @@ -322,7 +304,7 @@ fn pp_defs<'a>(env: &'a TypeEnv, prog: &'a IDLMergedProg) -> RcDoc<'a> { docs.append(kwd("public type")) .append(escape(id, false)) .append(" = ") - .append(pp_ty_rich(ty, syntax.map(|b| &b.typ))) + .append(pp_ty_rich(ty, syntax.map(|b| &b.typ.kind))) .append(";") })) } @@ -330,33 +312,27 @@ fn pp_defs<'a>(env: &'a TypeEnv, prog: &'a IDLMergedProg) -> RcDoc<'a> { fn pp_actor<'a>(ty: &'a Type, syntax: Option<&'a IDLActorType>) -> RcDoc<'a> { let self_doc = kwd("public type Self ="); match ty.as_ref() { - TypeInner::Service(ref serv) => { - if let Some(IDLActorType { - typ: IDLTypeKind::ServT(fields), - docs, - .. - }) = syntax - { - let docs = pp_docs(docs); - docs.append(self_doc).append(pp_service(serv, Some(fields))) - } else { - pp_service(serv, None) - } - } - TypeInner::Class(ref args, ref t) => { - if let Some(IDLActorType { - typ: IDLTypeKind::ClassT(_, syntax_t), - docs, - .. - }) = syntax - { - let docs = pp_docs(docs); - docs.append(self_doc) - .append(pp_class((args, t), Some(syntax_t))) - } else { - self_doc.append(pp_class((args, t), None)) - } - } + TypeInner::Service(ref serv) => match syntax { + Some(IDLActorType { typ, docs, .. }) => match &typ.kind { + IDLType::ServT(fields) => { + let docs = pp_docs(docs); + docs.append(self_doc).append(pp_service(serv, Some(fields))) + } + _ => pp_service(serv, None), + }, + None => pp_service(serv, None), + }, + TypeInner::Class(ref args, ref t) => match syntax { + Some(IDLActorType { typ, docs, .. }) => match &typ.kind { + IDLType::ClassT(_, syntax_t) => { + let docs = pp_docs(docs); + docs.append(self_doc) + .append(pp_class((args, t), Some(&syntax_t.kind))) + } + _ => self_doc.append(pp_class((args, t), None)), + }, + None => self_doc.append(pp_class((args, t), None)), + }, TypeInner::Var(_) => self_doc.append(pp_ty(ty)), _ => unreachable!(), } diff --git a/rust/candid_parser/src/bindings/rust.rs b/rust/candid_parser/src/bindings/rust.rs index 33c8a1d45..d698b230c 100644 --- a/rust/candid_parser/src/bindings/rust.rs +++ b/rust/candid_parser/src/bindings/rust.rs @@ -1,7 +1,7 @@ use super::analysis::{chase_actor, infer_rec}; use crate::{ configs::{ConfigState, ConfigTree, Configs, Context, StateElem}, - syntax::{self, IDLActorType, IDLMergedProg, IDLType, IDLTypeKind}, + syntax::{self, IDLActorType, IDLMergedProg, IDLType}, Deserialize, }; use candid::pretty::utils::*; @@ -150,24 +150,26 @@ fn find_field<'a>( if let Some(bs) = fields { if let Some(field) = bs.iter().find(|b| b.label == *label) { docs = pp_docs(&field.docs); - syntax_field_ty = Some(&field.typ); + syntax_field_ty = Some(&field.typ.kind); } }; (docs, syntax_field_ty) } fn record_syntax_fields(syntax: Option<&IDLType>) -> Option<&[syntax::TypeField]> { - syntax.and_then(|ty| match ty { - IDLTypeKind::RecordT(fields) => Some(fields.as_slice()), - _ => None, - }) + if let Some(IDLType::RecordT(syntax_fields)) = syntax { + Some(syntax_fields.as_slice()) + } else { + None + } } fn variant_syntax_fields(syntax: Option<&IDLType>) -> Option<&[syntax::TypeField]> { - syntax.and_then(|ty| match ty { - IDLTypeKind::VariantT(fields) => Some(fields.as_slice()), - _ => None, - }) + if let Some(IDLType::VariantT(syntax_fields)) = syntax { + Some(syntax_fields.as_slice()) + } else { + None + } } fn actor_methods(actor: Option<&IDLActorType>) -> &[syntax::Binding] { @@ -176,12 +178,15 @@ fn actor_methods(actor: Option<&IDLActorType>) -> &[syntax::Binding] { None => return &[], }; - match typ { - IDLTypeKind::ServT(methods) => methods, - IDLTypeKind::ClassT(_, inner) => match inner.as_ref() { - IDLTypeKind::ServT(methods) => methods, - _ => &[], - }, + match &typ.kind { + IDLType::ServT(methods) => methods, + IDLType::ClassT(_, inner) => { + if let IDLType::ServT(methods) = inner.as_ref() { + methods + } else { + &[] + } + } _ => &[], } } @@ -477,7 +482,7 @@ fn test_{test_name}() {{ .unwrap_or_else(|| to_identifier_case(id, IdentifierCase::UpperCamel).0); let syntax = self.prog.lookup(id); let syntax_ty = syntax - .map(|b| &b.typ) + .map(|b| &b.typ.kind) .or_else(|| self.generated_types.get(*id).map(|t| &**t)); let docs = syntax .map(|b| pp_docs(b.docs.as_ref())) @@ -919,20 +924,22 @@ impl<'b> NominalState<'_, 'b> { let res = match t.as_ref() { TypeInner::Opt(ty) => { path.push(TypePath::Opt); - let syntax_ty = syntax.and_then(|s| match s { - IDLTypeKind::OptT(inner) => Some(inner.as_ref()), - _ => None, - }); + let syntax_ty = if let Some(IDLType::OptT(inner)) = syntax { + Some(inner.as_ref()) + } else { + None + }; let ty = self.nominalize(env, path, ty, syntax_ty); path.pop(); TypeInner::Opt(ty) } TypeInner::Vec(ty) => { path.push(TypePath::Vec); - let syntax_ty = syntax.and_then(|s| match s { - IDLTypeKind::VecT(inner) => Some(inner.as_ref()), - _ => None, - }); + let syntax_ty = if let Some(IDLType::VecT(inner)) = syntax { + Some(inner.as_ref()) + } else { + None + }; let ty = self.nominalize(env, path, ty, syntax_ty); path.pop(); TypeInner::Vec(ty) @@ -951,8 +958,9 @@ impl<'b> NominalState<'_, 'b> { let elem = StateElem::Label(&lab); let old = self.state.push_state(&elem); path.push(TypePath::RecordField(id.to_string())); - let syntax_field = syntax_fields - .and_then(|s| s.iter().find(|f| f.label == **id).map(|f| &f.typ)); + let syntax_field = syntax_fields.and_then(|s| { + s.iter().find(|f| f.label == **id).map(|f| &f.typ.kind) + }); let ty = self.nominalize(env, path, ty, syntax_field); path.pop(); self.state.pop_state(old, elem); @@ -996,8 +1004,9 @@ impl<'b> NominalState<'_, 'b> { } else { path.push(TypePath::VariantField(id.to_string())); } - let syntax_field = syntax_fields - .and_then(|s| s.iter().find(|f| f.label == **id).map(|f| &f.typ)); + let syntax_field = syntax_fields.and_then(|s| { + s.iter().find(|f| f.label == **id).map(|f| &f.typ.kind) + }); let ty = self.nominalize(env, path, ty, syntax_field); path.pop(); self.state.pop_state(old, StateElem::Label(&lab)); @@ -1122,10 +1131,11 @@ impl<'b> NominalState<'_, 'b> { } }, TypeInner::Class(args, ty) => { - let syntax_ty = syntax.and_then(|s| match s { - IDLTypeKind::ClassT(_, syntax_ty) => Some(syntax_ty.as_ref()), - _ => None, - }); + let syntax_ty = if let Some(IDLType::ClassT(_, syntax_ty)) = syntax { + Some(syntax_ty.as_ref()) + } else { + None + }; TypeInner::Class( args.iter() .map(|arg| { @@ -1159,7 +1169,7 @@ impl<'b> NominalState<'_, 'b> { for (id, ty) in self.state.env.0.iter() { let elem = StateElem::Label(id); let old = self.state.push_state(&elem); - let syntax = prog.lookup(id).map(|t| &t.typ); + let syntax = prog.lookup(id).map(|t| &t.typ.kind); let ty = self.nominalize(&mut res, &mut vec![TypePath::Id(id.clone())], ty, syntax); res.0.insert(id.to_string(), ty); self.state.pop_state(old, elem); diff --git a/rust/candid_parser/src/bindings/typescript.rs b/rust/candid_parser/src/bindings/typescript.rs index ad6c03a5e..8284cebfe 100644 --- a/rust/candid_parser/src/bindings/typescript.rs +++ b/rust/candid_parser/src/bindings/typescript.rs @@ -1,5 +1,5 @@ use super::javascript::{ident, is_tuple_fields}; -use crate::syntax::{self, IDLMergedProg, IDLType, IDLTypeKind}; +use crate::syntax::{self, IDLMergedProg, IDLType}; use candid::pretty::utils::*; use candid::types::{Field, Function, Label, SharedLabel, Type, TypeEnv, TypeInner}; use pretty::RcDoc; @@ -17,7 +17,7 @@ fn find_field<'a>( if let Some(bs) = fields { if let Some(field) = bs.iter().find(|b| b.label == *label) { docs = pp_docs(&field.docs); - syntax_field_ty = Some(&field.typ); + syntax_field_ty = Some(&field.typ.kind); } }; (docs, syntax_field_ty) @@ -29,38 +29,23 @@ fn pp_ty_rich<'a>( syntax: Option<&'a IDLType>, is_ref: bool, ) -> RcDoc<'a> { - match ty.as_ref() { - TypeInner::Record(ref fields) => { - if let Some(IDLTypeKind::RecordT(syntax_fields)) = syntax { - return pp_record(env, fields, Some(syntax_fields), is_ref); - } - pp_record(env, fields, None, is_ref) + match (ty.as_ref(), syntax) { + (TypeInner::Record(ref fields), Some(IDLType::RecordT(syntax_fields))) => { + pp_record(env, fields, Some(syntax_fields), is_ref) } - TypeInner::Variant(ref fields) => { - if let Some(IDLTypeKind::VariantT(syntax_fields)) = syntax { - return pp_variant(env, fields, Some(syntax_fields), is_ref); - } - pp_variant(env, fields, None, is_ref) + (TypeInner::Variant(ref fields), Some(IDLType::VariantT(syntax_fields))) => { + pp_variant(env, fields, Some(syntax_fields), is_ref) } - TypeInner::Service(ref serv) => { - if let Some(IDLTypeKind::ServT(syntax_serv)) = syntax { - return pp_service(env, serv, Some(syntax_serv)); - } - pp_service(env, serv, None) + (TypeInner::Service(ref serv), Some(IDLType::ServT(syntax_serv))) => { + pp_service(env, serv, Some(syntax_serv)) } - TypeInner::Opt(ref t) => { - if let Some(IDLTypeKind::OptT(syntax_inner)) = syntax { - return pp_opt(env, t, Some(syntax_inner), is_ref); - } - pp_opt(env, t, None, is_ref) + (TypeInner::Opt(ref t), Some(IDLType::OptT(syntax_inner))) => { + pp_opt(env, t, Some(&syntax_inner.kind), is_ref) } - TypeInner::Vec(ref t) => { - if let Some(IDLTypeKind::VecT(syntax_inner)) = syntax { - return pp_vec(env, t, Some(syntax_inner), is_ref); - } - pp_vec(env, t, None, is_ref) + (TypeInner::Vec(ref t), Some(IDLType::VecT(syntax_inner))) => { + pp_vec(env, t, Some(&syntax_inner.kind), is_ref) } - _ => pp_ty(env, ty, is_ref), + (_, _) => pp_ty(env, ty, is_ref), } } @@ -281,7 +266,7 @@ fn pp_defs<'a>(env: &'a TypeEnv, def_list: &'a [&'a str], prog: &'a IDLMergedPro lines(def_list.iter().map(|id| { let ty = env.find_type(id).unwrap(); let syntax = prog.lookup(id); - let syntax_ty = syntax.map(|s| &s.typ); + let syntax_ty = syntax.map(|s| &s.typ.kind); let docs = syntax .map(|b| pp_docs(b.docs.as_ref())) .unwrap_or(RcDoc::nil()); @@ -323,10 +308,11 @@ fn pp_actor<'a>(env: &'a TypeEnv, ty: &'a Type, syntax: Option<&'a IDLType>) -> .append(str(id)) .append(str(" {}")), TypeInner::Class(_, t) => { - if let Some(IDLTypeKind::ClassT(_, syntax_t)) = syntax { - return pp_actor(env, t, Some(syntax_t)); + if let Some(IDLType::ClassT(_, syntax_t)) = syntax { + pp_actor(env, t, Some(&syntax_t.kind)) + } else { + pp_actor(env, t, None) } - pp_actor(env, t, None) } _ => unreachable!(), } @@ -347,11 +333,15 @@ import type { IDL } from '@dfinity/candid'; .as_ref() .map(|s| pp_docs(s.docs.as_ref())) .unwrap_or(RcDoc::nil()); - docs.append(pp_actor(env, actor, syntax_actor.as_ref().map(|s| &s.typ))) - .append(RcDoc::line()) - .append("export declare const idlFactory: IDL.InterfaceFactory;") - .append(RcDoc::line()) - .append("export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];") + docs.append(pp_actor( + env, + actor, + syntax_actor.as_ref().map(|s| &s.typ.kind), + )) + .append(RcDoc::line()) + .append("export declare const idlFactory: IDL.InterfaceFactory;") + .append(RcDoc::line()) + .append("export declare const init: (args: { IDL: typeof IDL }) => IDL.Type[];") } }; let doc = RcDoc::text(header) diff --git a/rust/candid_parser/src/grammar.lalrpop b/rust/candid_parser/src/grammar.lalrpop index 2b0849cd4..0987780dc 100644 --- a/rust/candid_parser/src/grammar.lalrpop +++ b/rust/candid_parser/src/grammar.lalrpop @@ -1,7 +1,10 @@ use super::test::{Assert, Input, Test}; use super::token::{error2, LexicalError, ParserError, Span, Token, TriviaMap}; use candid::{Principal, types::Label}; -use crate::syntax::spanned::{Binding, Dec, FuncType, IDLActorType, IDLInitArgs, IDLProg, IDLType, IDLTypeKind, IDLTypes, TypeField}; +use crate::syntax::{ + Binding, Dec, FuncType, IDLActorType, IDLInitArgs, IDLProg, IDLType, IDLTypeWithSpan, IDLTypes, + TypeField, +}; use crate::syntax::PrimType; use candid::types::value::{IDLField, IDLValue, IDLArgs, VariantValue}; use candid::types::{TypeEnv, FuncMode}; @@ -114,8 +117,7 @@ AnnVal: IDLValue = { => <>, > ":" > =>? { let env = TypeEnv::new(); - let spanless: crate::syntax::IDLType = typ.0.clone().into(); - let typ = crate::typing::ast_to_type(&env, &spanless).map_err(|e| error2(e, typ.1))?; + let typ = crate::typing::ast_to_type(&env, &typ.0.kind).map_err(|e| error2(e, typ.1))?; arg.0.annotate_type(true, &env, &typ).map_err(|e| error2(e, arg.1)) } } @@ -163,14 +165,14 @@ RecordField: IDLField = { // Type pub Typs: IDLTypes = TupTyp => IDLTypes { args:<> }; -pub Typ: IDLType = { +pub Typ: IDLTypeWithSpan = { PrimTyp => <>, - "opt" => IDLType::new(l..r, IDLTypeKind::OptT(Box::new(typ))), - "vec" => IDLType::new(l..r, IDLTypeKind::VecT(Box::new(typ))), + "opt" => IDLTypeWithSpan::new(l..r, IDLType::OptT(Box::new(typ))), + "vec" => IDLTypeWithSpan::new(l..r, IDLType::VecT(Box::new(typ))), "blob" => { let span = l..r; - let elem = IDLType::new(span.clone(), IDLTypeKind::PrimT(PrimType::Nat8)); - IDLType::new(span, IDLTypeKind::VecT(Box::new(elem))) + let elem = IDLTypeWithSpan::new(span.clone(), IDLType::PrimT(PrimType::Nat8)); + IDLTypeWithSpan::new(span, IDLType::VecT(Box::new(elem))) }, "record" "{" >> "}" =>? { let mut id: u32 = 0; @@ -196,27 +198,27 @@ pub Typ: IDLType = { }).collect(); fs.sort_unstable_by_key(|TypeField { label, .. }| label.get_id()); check_unique(fs.iter().map(|f| &f.label)).map_err(|e| error2(e, inner_span))?; - Ok(IDLType::new(span, IDLTypeKind::RecordT(fs))) + Ok(IDLTypeWithSpan::new(span, IDLType::RecordT(fs))) }, "variant" "{" >> "}" =>? { let span = l..r; let inner_span = fields.1.clone(); fields.0.sort_unstable_by_key(|TypeField { label, .. }| label.get_id()); check_unique(fields.0.iter().map(|f| &f.label)).map_err(|e| error2(e, inner_span))?; - Ok(IDLType::new(span, IDLTypeKind::VariantT(fields.0))) + Ok(IDLTypeWithSpan::new(span, IDLType::VariantT(fields.0))) }, - "func" => IDLType::new(l..r, IDLTypeKind::FuncT(typ)), - "service" => IDLType::new(l..r, IDLTypeKind::ServT(typ)), - "principal" => IDLType::new(l..r, IDLTypeKind::PrincipalT), + "func" => IDLTypeWithSpan::new(l..r, IDLType::FuncT(typ)), + "service" => IDLTypeWithSpan::new(l..r, IDLType::ServT(typ)), + "principal" => IDLTypeWithSpan::new(l..r, IDLType::PrincipalT), } -PrimTyp: IDLType = { - "null" => IDLType::new(l..r, IDLTypeKind::PrimT(PrimType::Null)), +PrimTyp: IDLTypeWithSpan = { + "null" => IDLTypeWithSpan::new(l..r, IDLType::PrimT(PrimType::Null)), => { let span = l..r; match PrimType::str_to_enum(&ident) { - Some(p) => IDLType::new(span, IDLTypeKind::PrimT(p)), - None => IDLType::new(span, IDLTypeKind::VarT(ident)), + Some(p) => IDLTypeWithSpan::new(span, IDLType::PrimT(p)), + None => IDLTypeWithSpan::new(span, IDLType::VarT(ident)), } }, } @@ -250,26 +252,26 @@ VariantFieldTyp: TypeField = { FieldTyp => <>, => TypeField { label: Label::Named(n), - typ: IDLType::new(l..r, IDLTypeKind::PrimT(PrimType::Null)), + typ: IDLTypeWithSpan::new(l..r, IDLType::PrimT(PrimType::Null)), docs: doc_comment.unwrap_or_default(), span: l..r, }, =>? Ok(TypeField { label: Label::Id(id), - typ: IDLType::new(l..r, IDLTypeKind::PrimT(PrimType::Null)), + typ: IDLTypeWithSpan::new(l..r, IDLType::PrimT(PrimType::Null)), docs: doc_comment.unwrap_or_default(), span: l..r, }), } -TupTyp: Vec = "(" > ")" => <>; +TupTyp: Vec = "(" > ")" => <>; FuncTyp: FuncType = { "->" => FuncType { modes, args, rets }, } -ArgTyp: IDLType = { +ArgTyp: IDLTypeWithSpan = { Typ => <>, Name ":" => <>, } @@ -292,13 +294,13 @@ ActorTyp: Vec = { MethTyp: Binding = { ":" => Binding { id: n, - typ: IDLType::new(l..r, IDLTypeKind::FuncT(f)), + typ: IDLTypeWithSpan::new(l..r, IDLType::FuncT(f)), docs: doc_comment.unwrap_or_default(), span: l..r, }, ":" => Binding { id: n, - typ: IDLType::new(l..r, IDLTypeKind::VarT(id)), + typ: IDLTypeWithSpan::new(l..r, IDLType::VarT(id)), docs: doc_comment.unwrap_or_default(), span: l..r, }, @@ -321,9 +323,9 @@ RecoverDef: Result = { => Err(err.error), } -Actor: IDLType = { - => IDLType::new(l..r, IDLTypeKind::ServT(typ)), - => IDLType::new(l..r, IDLTypeKind::VarT(name)), +Actor: IDLTypeWithSpan = { + => IDLTypeWithSpan::new(l..r, IDLType::ServT(typ)), + => IDLTypeWithSpan::new(l..r, IDLType::VarT(name)), } MainActor: IDLActorType = { @@ -333,7 +335,7 @@ MainActor: IDLActorType = { span: l..r, }, "service" "id"? ":" "->" ";"? => IDLActorType { - typ: IDLType::new(l..r, IDLTypeKind::ClassT(args, Box::new(t))), + typ: IDLTypeWithSpan::new(l..r, IDLType::ClassT(args, Box::new(t))), docs: doc_comment.unwrap_or_default(), span: l..r, }, @@ -398,19 +400,15 @@ Assert: Assert = > =>? { Assertion: Assert = { ":" => { - let typ: Vec = typ.into_iter().map(Into::into).collect(); Assert { left, right: None, typ, pass: true, desc } }, "!:" => { - let typ: Vec = typ.into_iter().map(Into::into).collect(); Assert { left, right: None, typ, pass: false, desc } }, "==" ":" => { - let typ: Vec = typ.into_iter().map(Into::into).collect(); Assert { left, right: Some(right), typ, pass: true, desc } }, "!=" ":" => { - let typ: Vec = typ.into_iter().map(Into::into).collect(); Assert { left, right: Some(right), typ, pass: false, desc } }, } diff --git a/rust/candid_parser/src/syntax/mod.rs b/rust/candid_parser/src/syntax/mod.rs index 337b5dee1..7cce01dbe 100644 --- a/rust/candid_parser/src/syntax/mod.rs +++ b/rust/candid_parser/src/syntax/mod.rs @@ -1,11 +1,10 @@ mod pretty; -pub mod spanned; pub use pretty::pretty_print; use crate::{ error, - token::{LexicalError, ParserError, Token, TriviaMap}, + token::{LexicalError, ParserError, Span, Token, TriviaMap}, }; use anyhow::{anyhow, bail, Context, Result}; use candid::{ @@ -14,22 +13,57 @@ use candid::{ }; use std::collections::HashMap; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Spanned { + pub value: T, + pub span: Span, +} + +impl Spanned { + pub fn new(value: T, span: Span) -> Self { + Spanned { value, span } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IDLTypeWithSpan { + pub span: Span, + pub kind: IDLType, +} + +impl IDLTypeWithSpan { + pub fn new(span: Span, kind: IDLType) -> Self { + IDLTypeWithSpan { span, kind } + } + + pub fn synthetic(kind: IDLType) -> Self { + IDLTypeWithSpan { span: 0..0, kind } + } +} + +impl std::str::FromStr for IDLTypeWithSpan { + type Err = error::Error; + fn from_str(str: &str) -> error::Result { + let trivia = super::token::TriviaMap::default(); + let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); + Ok(super::grammar::TypParser::new().parse(Some(&trivia), lexer)?) + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum IDLType { PrimT(PrimType), VarT(String), FuncT(FuncType), - OptT(Box), - VecT(Box), + OptT(Box), + VecT(Box), RecordT(Vec), VariantT(Vec), ServT(Vec), - ClassT(Vec, Box), + ClassT(Vec, Box), PrincipalT, } -pub type IDLTypeKind = IDLType; - impl IDLType { pub fn is_tuple(&self) -> bool { match self { @@ -46,56 +80,48 @@ impl IDLType { } } -impl std::str::FromStr for IDLType { - type Err = error::Error; - fn from_str(str: &str) -> error::Result { - Ok(spanned::IDLType::from_str(str)?.into()) +impl From for IDLType { + fn from(value: IDLTypeWithSpan) -> Self { + value.kind } } -impl From for IDLType { - fn from(value: spanned::IDLType) -> Self { - match value.kind { - spanned::IDLTypeKind::PrimT(p) => IDLType::PrimT(p), - spanned::IDLTypeKind::VarT(v) => IDLType::VarT(v), - spanned::IDLTypeKind::FuncT(f) => IDLType::FuncT(f.into()), - spanned::IDLTypeKind::OptT(t) => IDLType::OptT(Box::new((*t).into())), - spanned::IDLTypeKind::VecT(t) => IDLType::VecT(Box::new((*t).into())), - spanned::IDLTypeKind::RecordT(fs) => { - IDLType::RecordT(fs.into_iter().map(Into::into).collect()) - } - spanned::IDLTypeKind::VariantT(fs) => { - IDLType::VariantT(fs.into_iter().map(Into::into).collect()) - } - spanned::IDLTypeKind::ServT(bs) => { - IDLType::ServT(bs.into_iter().map(Into::into).collect()) - } - spanned::IDLTypeKind::ClassT(args, ty) => IDLType::ClassT( - args.into_iter().map(Into::into).collect(), - Box::new((*ty).into()), - ), - spanned::IDLTypeKind::PrincipalT => IDLType::PrincipalT, - } +impl AsRef for IDLTypeWithSpan { + fn as_ref(&self) -> &IDLType { + &self.kind } } -#[derive(Debug, Clone)] -pub struct IDLTypes { - pub args: Vec, +impl AsRef for Box { + fn as_ref(&self) -> &IDLType { + &self.kind + } } -impl std::str::FromStr for IDLTypes { +impl AsRef for IDLType { + fn as_ref(&self) -> &IDLType { + self + } +} + +impl std::str::FromStr for IDLType { type Err = error::Error; fn from_str(str: &str) -> error::Result { - Ok(spanned::IDLTypes::from_str(str)?.into()) + Ok(IDLTypeWithSpan::from_str(str)?.kind) } } -impl From for IDLTypes { - fn from(types: spanned::IDLTypes) -> Self { - IDLTypes { - args: types.args.into_iter().map(Into::into).collect(), - } +#[derive(Debug, Clone)] +pub struct IDLTypes { + pub args: Vec, +} + +impl std::str::FromStr for IDLTypes { + type Err = error::Error; + fn from_str(str: &str) -> error::Result { + let trivia = super::token::TriviaMap::default(); + let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); + Ok(super::grammar::TypsParser::new().parse(Some(&trivia), lexer)?) } } @@ -142,90 +168,45 @@ pub enum PrimType { #[derive(Debug, Clone, PartialEq, Eq)] pub struct FuncType { pub modes: Vec, - pub args: Vec, - pub rets: Vec, -} - -impl From for FuncType { - fn from(func: spanned::FuncType) -> Self { - FuncType { - modes: func.modes, - args: func.args.into_iter().map(Into::into).collect(), - rets: func.rets.into_iter().map(Into::into).collect(), - } - } + pub args: Vec, + pub rets: Vec, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct TypeField { pub label: Label, - pub typ: IDLType, + pub typ: IDLTypeWithSpan, pub docs: Vec, -} - -impl From for TypeField { - fn from(field: spanned::TypeField) -> Self { - TypeField { - label: field.label, - typ: field.typ.into(), - docs: field.docs, - } - } + pub span: Span, } #[derive(Debug)] pub enum Dec { TypD(Binding), - ImportType(String), - ImportServ(String), -} - -impl From for Dec { - fn from(dec: spanned::Dec) -> Self { - match dec { - spanned::Dec::TypD(binding) => Dec::TypD(binding.into()), - spanned::Dec::ImportType { path, .. } => Dec::ImportType(path), - spanned::Dec::ImportServ { path, .. } => Dec::ImportServ(path), - } - } + ImportType { path: String, span: Span }, + ImportServ { path: String, span: Span }, } #[derive(Debug, Clone, PartialEq, Eq)] pub struct Binding { pub id: String, - pub typ: IDLType, + pub typ: IDLTypeWithSpan, pub docs: Vec, -} - -impl From for Binding { - fn from(binding: spanned::Binding) -> Self { - Binding { - id: binding.id, - typ: binding.typ.into(), - docs: binding.docs, - } - } + pub span: Span, } #[derive(Debug, Clone)] pub struct IDLActorType { - pub typ: IDLType, + pub typ: IDLTypeWithSpan, pub docs: Vec, -} - -impl From for IDLActorType { - fn from(actor: spanned::IDLActorType) -> Self { - IDLActorType { - typ: actor.typ.into(), - docs: actor.docs, - } - } + pub span: Span, } #[derive(Debug)] pub struct IDLProg { pub decs: Vec, pub actor: Option, + pub span: Span, } impl IDLProg { @@ -246,7 +227,7 @@ impl IDLProg { where I: IntoIterator>, { - spanned::IDLProg::parse_from_tokens(trivia, tokens).map(Into::into) + super::grammar::IDLProgParser::new().parse(trivia, tokens) } pub fn parse_lossy_from_tokens( @@ -256,45 +237,35 @@ impl IDLProg { where I: IntoIterator>, { - let (res, errors) = spanned::IDLProg::parse_lossy_from_tokens(trivia, tokens); - (res.map(Into::into), errors) + match super::grammar::IDLProgLossyParser::new().parse(trivia, tokens) { + Ok(result) => result, + Err(err) => (None, vec![err]), + } } } impl std::str::FromStr for IDLProg { type Err = error::Error; fn from_str(str: &str) -> error::Result { - Ok(spanned::IDLProg::from_str(str)?.into()) - } -} - -impl From for IDLProg { - fn from(prog: spanned::IDLProg) -> Self { - IDLProg { - decs: prog.decs.into_iter().map(Into::into).collect(), - actor: prog.actor.map(Into::into), - } + let trivia = super::token::TriviaMap::default(); + let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); + Ok(Self::parse_from_tokens(Some(&trivia), lexer)?) } } #[derive(Debug)] pub struct IDLInitArgs { pub decs: Vec, - pub args: Vec, + pub args: Vec, + pub span: Span, } impl std::str::FromStr for IDLInitArgs { type Err = error::Error; fn from_str(str: &str) -> error::Result { - Ok(spanned::IDLInitArgs::from_str(str)?.into()) - } -} -impl From for IDLInitArgs { - fn from(args: spanned::IDLInitArgs) -> Self { - IDLInitArgs { - decs: args.decs.into_iter().map(Into::into).collect(), - args: args.args.into_iter().map(Into::into).collect(), - } + let trivia = super::token::TriviaMap::default(); + let lexer = super::token::Tokenizer::new_with_trivia(str, trivia.clone()); + Ok(super::grammar::IDLInitArgsParser::new().parse(Some(&trivia), lexer)?) } } @@ -338,32 +309,35 @@ impl IDLMergedProg { } pub fn resolve_actor(&self) -> Result> { - let (init_args, top_level_docs, mut methods) = match &self.main_actor { + let mut init_args: Option> = None; + let mut top_level_docs: Vec = vec![]; + let mut actor_span: Span = 0..0; + let mut methods = match &self.main_actor { None => { if self.service_imports.is_empty() { return Ok(None); - } else { - (None, vec![], vec![]) + } + vec![] + } + Some(actor) if self.service_imports.is_empty() => return Ok(Some(actor.clone())), + Some(actor) => { + top_level_docs = actor.docs.clone(); + actor_span = actor.span.clone(); + match &actor.typ.kind { + IDLType::ClassT(args, inner) => { + init_args = Some(args.clone()); + self.chase_service((**inner).clone(), None)? + } + _ => self.chase_service(actor.typ.clone(), None)?, } } - Some(t) if self.service_imports.is_empty() => return Ok(Some(t.clone())), - Some(IDLActorType { - typ: IDLType::ClassT(args, inner), - docs, - }) => ( - Some(args.clone()), - docs.clone(), - self.chase_service(*inner.clone(), None)?, - ), - Some(ty) => ( - None, - ty.docs.clone(), - self.chase_service(ty.typ.clone(), None)?, - ), }; for (name, typ) in &self.service_imports { methods.extend(self.chase_service(typ.typ.clone(), Some(name))?); + if top_level_docs.is_empty() { + top_level_docs = typ.docs.clone(); + } } let mut hashes: HashMap = HashMap::new(); @@ -374,20 +348,27 @@ impl IDLMergedProg { } } + let service_type = IDLTypeWithSpan::synthetic(IDLType::ServT(methods)); let typ = if let Some(args) = init_args { - IDLType::ClassT(args, Box::new(IDLType::ServT(methods))) + IDLTypeWithSpan::synthetic(IDLType::ClassT(args, Box::new(service_type.clone()))) } else { - IDLType::ServT(methods) + service_type }; + Ok(Some(IDLActorType { typ, docs: top_level_docs, + span: actor_span, })) } // NOTE: We don't worry about cyclic type definitions, as we rule those out earlier when checking the type decs - fn chase_service(&self, ty: IDLType, import_name: Option<&str>) -> Result> { - match ty { + fn chase_service( + &self, + ty: IDLTypeWithSpan, + import_name: Option<&str>, + ) -> Result> { + match ty.kind { IDLType::VarT(v) => { let resolved = self .typ_decs @@ -397,9 +378,9 @@ impl IDLMergedProg { self.chase_service(resolved.typ.clone(), import_name) } IDLType::ServT(bindings) => Ok(bindings), - ty => Err(import_name + ty_kind => Err(import_name .map(|name| anyhow!("Imported service file \"{name}\" has a service constructor")) - .unwrap_or(anyhow!("not a service type: {:?}", ty))), + .unwrap_or(anyhow!("not a service type: {:?}", ty_kind))), } } } diff --git a/rust/candid_parser/src/syntax/pretty.rs b/rust/candid_parser/src/syntax/pretty.rs index 9d57f2393..444b103b3 100644 --- a/rust/candid_parser/src/syntax/pretty.rs +++ b/rust/candid_parser/src/syntax/pretty.rs @@ -6,47 +6,49 @@ use crate::{ utils::{concat, enclose, enclose_space, ident, kwd, lines, str, INDENT_SPACE, LINE_WIDTH}, }, syntax::{ - Binding, FuncType, IDLActorType, IDLMergedProg, IDLType, IDLTypeKind, PrimType, TypeField, + Binding, FuncType, IDLActorType, IDLMergedProg, IDLType, IDLTypeWithSpan, PrimType, + TypeField, }, }; fn pp_ty(ty: &IDLType) -> RcDoc<'_> { + use IDLType::*; match ty { - IDLTypeKind::PrimT(PrimType::Null) => str("null"), - IDLTypeKind::PrimT(PrimType::Bool) => str("bool"), - IDLTypeKind::PrimT(PrimType::Nat) => str("nat"), - IDLTypeKind::PrimT(PrimType::Int) => str("int"), - IDLTypeKind::PrimT(PrimType::Nat8) => str("nat8"), - IDLTypeKind::PrimT(PrimType::Nat16) => str("nat16"), - IDLTypeKind::PrimT(PrimType::Nat32) => str("nat32"), - IDLTypeKind::PrimT(PrimType::Nat64) => str("nat64"), - IDLTypeKind::PrimT(PrimType::Int8) => str("int8"), - IDLTypeKind::PrimT(PrimType::Int16) => str("int16"), - IDLTypeKind::PrimT(PrimType::Int32) => str("int32"), - IDLTypeKind::PrimT(PrimType::Int64) => str("int64"), - IDLTypeKind::PrimT(PrimType::Float32) => str("float32"), - IDLTypeKind::PrimT(PrimType::Float64) => str("float64"), - IDLTypeKind::PrimT(PrimType::Text) => str("text"), - IDLTypeKind::PrimT(PrimType::Reserved) => str("reserved"), - IDLTypeKind::PrimT(PrimType::Empty) => str("empty"), - IDLTypeKind::VarT(ref s) => str(s), - IDLTypeKind::PrincipalT => str("principal"), - IDLTypeKind::OptT(ref t) => pp_opt(t), - IDLTypeKind::VecT(ref t) => pp_vec(t), - IDLTypeKind::RecordT(ref fs) => pp_record(fs, ty.is_tuple()), - IDLTypeKind::VariantT(ref fs) => pp_variant(fs), - IDLTypeKind::FuncT(ref func) => pp_function(func), - IDLTypeKind::ServT(ref serv) => pp_service(serv), - IDLTypeKind::ClassT(ref args, ref t) => pp_class(args, t), + PrimT(PrimType::Null) => str("null"), + PrimT(PrimType::Bool) => str("bool"), + PrimT(PrimType::Nat) => str("nat"), + PrimT(PrimType::Int) => str("int"), + PrimT(PrimType::Nat8) => str("nat8"), + PrimT(PrimType::Nat16) => str("nat16"), + PrimT(PrimType::Nat32) => str("nat32"), + PrimT(PrimType::Nat64) => str("nat64"), + PrimT(PrimType::Int8) => str("int8"), + PrimT(PrimType::Int16) => str("int16"), + PrimT(PrimType::Int32) => str("int32"), + PrimT(PrimType::Int64) => str("int64"), + PrimT(PrimType::Float32) => str("float32"), + PrimT(PrimType::Float64) => str("float64"), + PrimT(PrimType::Text) => str("text"), + PrimT(PrimType::Reserved) => str("reserved"), + PrimT(PrimType::Empty) => str("empty"), + VarT(ref s) => str(s), + PrincipalT => str("principal"), + OptT(ref t) => pp_opt(t), + VecT(ref t) => pp_vec(t), + RecordT(ref fs) => pp_record(fs, ty.is_tuple()), + VariantT(ref fs) => pp_variant(fs), + FuncT(ref func) => pp_function(func), + ServT(ref serv) => pp_service(serv), + ClassT(ref args, ref t) => pp_class(args, t), } } fn pp_field(field: &TypeField, is_variant: bool) -> RcDoc<'_> { let docs = pp_docs(&field.docs); - let ty_doc = if is_variant && matches!(field.typ, IDLTypeKind::PrimT(PrimType::Null)) { + let ty_doc = if is_variant && matches!(field.typ.kind, IDLType::PrimT(PrimType::Null)) { RcDoc::nil() } else { - kwd(" :").append(pp_ty(&field.typ)) + kwd(" :").append(pp_ty(&field.typ.kind)) }; docs.append(pp_label_raw(&field.label)).append(ty_doc) } @@ -56,21 +58,21 @@ fn pp_fields(fs: &[TypeField], is_variant: bool) -> RcDoc<'_> { enclose_space("{", concat(fields, ";"), "}") } -fn pp_opt(ty: &IDLType) -> RcDoc<'_> { - kwd("opt").append(pp_ty(ty)) +fn pp_opt(ty: &IDLTypeWithSpan) -> RcDoc<'_> { + kwd("opt").append(pp_ty(&ty.kind)) } -fn pp_vec(ty: &IDLType) -> RcDoc<'_> { - if matches!(ty, IDLTypeKind::PrimT(PrimType::Nat8)) { +fn pp_vec(ty: &IDLTypeWithSpan) -> RcDoc<'_> { + if matches!(ty.kind, IDLType::PrimT(PrimType::Nat8)) { str("blob") } else { - kwd("vec").append(pp_ty(ty)) + kwd("vec").append(pp_ty(&ty.kind)) } } fn pp_record(fs: &[TypeField], is_tuple: bool) -> RcDoc<'_> { if is_tuple { - let tuple = concat(fs.iter().map(|f| pp_ty(&f.typ)), ";"); + let tuple = concat(fs.iter().map(|f| pp_ty(&f.typ.kind)), ";"); kwd("record").append(enclose_space("{", tuple, "}")) } else { kwd("record").append(pp_fields(fs, false)) @@ -95,12 +97,12 @@ fn pp_method(func: &FuncType) -> RcDoc<'_> { .nest(INDENT_SPACE) } -fn pp_args(args: &[IDLType]) -> RcDoc<'_> { - let doc = concat(args.iter().map(pp_ty), ","); +fn pp_args(args: &[IDLTypeWithSpan]) -> RcDoc<'_> { + let doc = concat(args.iter().map(|ty| pp_ty(&ty.kind)), ","); enclose("(", doc, ")") } -fn pp_rets(rets: &[IDLType]) -> RcDoc<'_> { +fn pp_rets(rets: &[IDLTypeWithSpan]) -> RcDoc<'_> { pp_args(rets) } @@ -111,9 +113,9 @@ fn pp_service(methods: &[Binding]) -> RcDoc<'_> { fn pp_service_methods(methods: &[Binding]) -> RcDoc<'_> { let methods = methods.iter().map(|b| { let docs = pp_docs(&b.docs); - let func_doc = match &b.typ { - IDLTypeKind::FuncT(ref f) => pp_method(f), - IDLTypeKind::VarT(_) => pp_ty(&b.typ), + let func_doc = match &b.typ.kind { + IDLType::FuncT(ref f) => pp_method(f), + IDLType::VarT(_) => pp_ty(&b.typ.kind), _ => unreachable!(), }; docs.append(pp_text(&b.id)) @@ -124,11 +126,11 @@ fn pp_service_methods(methods: &[Binding]) -> RcDoc<'_> { enclose_space("{", doc, "}") } -fn pp_class<'a>(args: &'a [IDLType], t: &'a IDLType) -> RcDoc<'a> { +fn pp_class<'a>(args: &'a [IDLTypeWithSpan], t: &'a IDLTypeWithSpan) -> RcDoc<'a> { let doc = pp_args(args).append(" ->").append(RcDoc::space()); - match t { - IDLTypeKind::ServT(ref serv) => doc.append(pp_service_methods(serv)), - IDLTypeKind::VarT(ref s) => doc.append(s), + match &t.kind { + IDLType::ServT(ref serv) => doc.append(pp_service_methods(serv)), + IDLType::VarT(ref s) => doc.append(s), _ => unreachable!(), } } @@ -139,16 +141,16 @@ fn pp_defs(prog: &IDLMergedProg) -> RcDoc<'_> { docs.append(kwd("type")) .append(ident(&b.id)) .append(kwd("=")) - .append(pp_ty(&b.typ)) + .append(pp_ty(&b.typ.kind)) .append(";") })) } fn pp_actor(actor: &IDLActorType) -> RcDoc<'_> { let docs = pp_docs(&actor.docs); - let service_doc = match &actor.typ { - IDLTypeKind::ServT(ref serv) => pp_service_methods(serv), - IDLTypeKind::VarT(_) | IDLTypeKind::ClassT(_, _) => pp_ty(&actor.typ), + let service_doc = match &actor.typ.kind { + IDLType::ServT(ref serv) => pp_service_methods(serv), + IDLType::VarT(_) | IDLType::ClassT(_, _) => pp_ty(&actor.typ.kind), _ => unreachable!(), }; docs.append(kwd("service :")).append(service_doc) diff --git a/rust/candid_parser/src/syntax/spanned.rs b/rust/candid_parser/src/syntax/spanned.rs deleted file mode 100644 index bad48cea8..000000000 --- a/rust/candid_parser/src/syntax/spanned.rs +++ /dev/null @@ -1,302 +0,0 @@ -use super::PrimType; -use crate::{ - error, - token::{LexicalError, ParserError, Span, Token, TriviaMap}, -}; -use anyhow::{anyhow, bail, Context, Result}; -use candid::{ - idl_hash, - types::{FuncMode, Label}, -}; -use std::collections::HashMap; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Spanned { - pub value: T, - pub span: Span, -} - -impl Spanned { - pub fn new(value: T, span: Span) -> Self { - Spanned { value, span } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct IDLType { - pub span: Span, - pub kind: IDLTypeKind, -} - -impl IDLType { - pub fn new(span: Span, kind: IDLTypeKind) -> Self { - IDLType { span, kind } - } - - pub fn is_tuple(&self) -> bool { - match &self.kind { - IDLTypeKind::RecordT(ref fs) => fs - .iter() - .enumerate() - .all(|(i, field)| field.label.get_id() == (i as u32)), - _ => false, - } - } - - pub fn synthetic(kind: IDLTypeKind) -> Self { - IDLType { span: 0..0, kind } - } -} - -impl std::str::FromStr for IDLType { - type Err = error::Error; - fn from_str(str: &str) -> error::Result { - let trivia = crate::token::TriviaMap::default(); - let lexer = crate::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(crate::grammar::TypParser::new().parse(Some(&trivia), lexer)?) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum IDLTypeKind { - PrimT(PrimType), - VarT(String), - FuncT(FuncType), - OptT(Box), - VecT(Box), - RecordT(Vec), - VariantT(Vec), - ServT(Vec), - ClassT(Vec, Box), - PrincipalT, -} - -#[derive(Debug, Clone)] -pub struct IDLTypes { - pub args: Vec, -} - -impl std::str::FromStr for IDLTypes { - type Err = error::Error; - fn from_str(str: &str) -> error::Result { - let trivia = crate::token::TriviaMap::default(); - let lexer = crate::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(crate::grammar::TypsParser::new().parse(Some(&trivia), lexer)?) - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct FuncType { - pub modes: Vec, - pub args: Vec, - pub rets: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct TypeField { - pub label: Label, - pub typ: IDLType, - pub docs: Vec, - pub span: Span, -} - -#[derive(Debug)] -pub enum Dec { - TypD(Binding), - ImportType { path: String, span: Span }, - ImportServ { path: String, span: Span }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Binding { - pub id: String, - pub typ: IDLType, - pub docs: Vec, - pub span: Span, -} - -#[derive(Debug, Clone)] -pub struct IDLActorType { - pub typ: IDLType, - pub docs: Vec, - pub span: Span, -} - -#[derive(Debug)] -pub struct IDLProg { - pub decs: Vec, - pub actor: Option, - pub span: Span, -} - -impl IDLProg { - pub fn typ_decs(decs: Vec) -> impl Iterator { - decs.into_iter().filter_map(|d| { - if let Dec::TypD(bindings) = d { - Some(bindings) - } else { - None - } - }) - } - - pub fn parse_from_tokens( - trivia: Option<&TriviaMap>, - tokens: I, - ) -> std::result::Result - where - I: IntoIterator>, - { - crate::grammar::IDLProgParser::new().parse(trivia, tokens) - } - - pub fn parse_lossy_from_tokens( - trivia: Option<&TriviaMap>, - tokens: I, - ) -> (Option, Vec) - where - I: IntoIterator>, - { - match crate::grammar::IDLProgLossyParser::new().parse(trivia, tokens) { - Ok(result) => result, - Err(err) => (None, vec![err]), - } - } -} - -impl std::str::FromStr for IDLProg { - type Err = error::Error; - fn from_str(str: &str) -> error::Result { - let trivia = crate::token::TriviaMap::default(); - let lexer = crate::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(Self::parse_from_tokens(Some(&trivia), lexer)?) - } -} - -#[derive(Debug)] -pub struct IDLInitArgs { - pub decs: Vec, - pub args: Vec, - pub span: Span, -} - -impl std::str::FromStr for IDLInitArgs { - type Err = error::Error; - fn from_str(str: &str) -> error::Result { - let trivia = crate::token::TriviaMap::default(); - let lexer = crate::token::Tokenizer::new_with_trivia(str, trivia.clone()); - Ok(crate::grammar::IDLInitArgsParser::new().parse(Some(&trivia), lexer)?) - } -} - -#[derive(Debug)] -pub struct IDLMergedProg { - typ_decs: Vec, - main_actor: Option, - service_imports: Vec<(String, IDLActorType)>, -} - -impl IDLMergedProg { - pub fn new(prog: IDLProg) -> IDLMergedProg { - IDLMergedProg { - typ_decs: IDLProg::typ_decs(prog.decs).collect(), - main_actor: prog.actor, - service_imports: vec![], - } - } - - pub fn merge(&mut self, is_service_import: bool, name: String, prog: IDLProg) -> Result<()> { - self.typ_decs.extend(IDLProg::typ_decs(prog.decs)); - if is_service_import { - let actor = prog - .actor - .with_context(|| format!("Imported service file \"{name}\" has no main service"))?; - self.service_imports.push((name, actor)); - } - Ok(()) - } - - pub fn lookup(&self, id: &str) -> Option<&Binding> { - self.typ_decs.iter().find(|b| b.id == id) - } - - pub fn decs(&self) -> Vec { - self.typ_decs.iter().map(|b| Dec::TypD(b.clone())).collect() - } - - pub fn bindings(&self) -> impl Iterator { - self.typ_decs.iter() - } - - pub fn resolve_actor(&self) -> Result> { - let mut init_args: Option> = None; - let mut top_level_docs: Vec = vec![]; - let mut actor_span: Span = 0..0; - let mut methods = match &self.main_actor { - None => { - if self.service_imports.is_empty() { - return Ok(None); - } - vec![] - } - Some(actor) if self.service_imports.is_empty() => return Ok(Some(actor.clone())), - Some(actor) => { - top_level_docs = actor.docs.clone(); - actor_span = actor.span.clone(); - match &actor.typ.kind { - IDLTypeKind::ClassT(args, inner) => { - init_args = Some(args.clone()); - self.chase_service((**inner).clone(), None)? - } - _ => self.chase_service(actor.typ.clone(), None)?, - } - } - }; - - for (name, typ) in &self.service_imports { - methods.extend(self.chase_service(typ.typ.clone(), Some(name))?); - if top_level_docs.is_empty() { - top_level_docs = typ.docs.clone(); - } - } - - let mut hashes: HashMap = HashMap::new(); - for method in &methods { - let name = &method.id; - if let Some(previous) = hashes.insert(idl_hash(name), name) { - bail!("Duplicate imported method name: label '{name}' hash collision with '{previous}'") - } - } - - let service_type = IDLType::synthetic(IDLTypeKind::ServT(methods)); - let typ = if let Some(args) = init_args { - IDLType::synthetic(IDLTypeKind::ClassT(args, Box::new(service_type.clone()))) - } else { - service_type - }; - - Ok(Some(IDLActorType { - typ, - docs: top_level_docs, - span: actor_span, - })) - } - - // NOTE: We don't worry about cyclic type definitions, as we rule those out earlier when checking the type decs - fn chase_service(&self, ty: IDLType, import_name: Option<&str>) -> Result> { - match ty.kind { - IDLTypeKind::VarT(v) => { - let resolved = self - .typ_decs - .iter() - .find(|b| b.id == v) - .with_context(|| format!("Unbound type identifier {v}"))?; - self.chase_service(resolved.typ.clone(), import_name) - } - IDLTypeKind::ServT(bindings) => Ok(bindings), - ty_kind => Err(import_name - .map(|name| anyhow!("Imported service file \"{name}\" has a service constructor")) - .unwrap_or(anyhow!("not a service type: {:?}", ty_kind))), - } - } -} diff --git a/rust/candid_parser/src/test.rs b/rust/candid_parser/src/test.rs index 10735e941..5ee3bd1fa 100644 --- a/rust/candid_parser/src/test.rs +++ b/rust/candid_parser/src/test.rs @@ -1,5 +1,5 @@ use super::typing::check_prog; -use crate::syntax::{Dec, IDLProg, IDLType}; +use crate::syntax::{Dec, IDLProg, IDLTypeWithSpan}; use crate::{Error, Result}; use candid::types::value::IDLArgs; use candid::types::{Type, TypeEnv}; @@ -7,7 +7,7 @@ use candid::DecoderConfig; const DECODING_COST: usize = 20_000_000; -type TupType = Vec; +type TupType = Vec; pub struct Test { pub defs: Vec, @@ -151,6 +151,7 @@ pub fn check(test: Test) -> Result<()> { let prog = IDLProg { decs: test.defs, actor: None, + span: 0..0, }; check_prog(&mut env, &prog)?; let mut count = 0; @@ -158,7 +159,7 @@ pub fn check(test: Test) -> Result<()> { print!("Checking {} {}...", i + 1, assert.desc()); let mut types = Vec::new(); for ty in assert.typ.iter() { - types.push(super::typing::ast_to_type(&env, ty)?); + types.push(super::typing::ast_to_type(&env, ty.as_ref())?); } let input = assert.left.parse(&env, &types); let pass = if let Some(assert_right) = &assert.right { diff --git a/rust/candid_parser/src/typing.rs b/rust/candid_parser/src/typing.rs index 85d1b4918..1d6f80747 100644 --- a/rust/candid_parser/src/typing.rs +++ b/rust/candid_parser/src/typing.rs @@ -1,8 +1,8 @@ use crate::{ pretty_parse, syntax::{ - Binding, Dec, IDLActorType, IDLInitArgs, IDLMergedProg, IDLProg, IDLType, IDLTypeKind, - PrimType, TypeField, + Binding, Dec, IDLActorType, IDLInitArgs, IDLMergedProg, IDLProg, IDLType, PrimType, + TypeField, }, Error, Result, }; @@ -47,31 +47,35 @@ fn check_prim(prim: &PrimType) -> Type { .into() } -pub fn check_type(env: &Env, t: &IDLType) -> Result { +pub fn check_type(env: &Env, t: T) -> Result +where + T: AsRef, +{ + let t = t.as_ref(); match t { - IDLTypeKind::PrimT(prim) => Ok(check_prim(prim)), - IDLTypeKind::VarT(id) => { + IDLType::PrimT(prim) => Ok(check_prim(prim)), + IDLType::VarT(id) => { env.te.find_type(id)?; Ok(TypeInner::Var(id.to_string()).into()) } - IDLTypeKind::OptT(t) => { + IDLType::OptT(t) => { let t = check_type(env, t)?; Ok(TypeInner::Opt(t).into()) } - IDLTypeKind::VecT(t) => { + IDLType::VecT(t) => { let t = check_type(env, t)?; Ok(TypeInner::Vec(t).into()) } - IDLTypeKind::RecordT(fs) => { + IDLType::RecordT(fs) => { let fs = check_fields(env, fs)?; Ok(TypeInner::Record(fs).into()) } - IDLTypeKind::VariantT(fs) => { + IDLType::VariantT(fs) => { let fs = check_fields(env, fs)?; Ok(TypeInner::Variant(fs).into()) } - IDLTypeKind::PrincipalT => Ok(TypeInner::Principal.into()), - IDLTypeKind::FuncT(func) => { + IDLType::PrincipalT => Ok(TypeInner::Principal.into()), + IDLType::FuncT(func) => { let mut t1 = Vec::new(); for arg in func.args.iter() { t1.push(check_type(env, arg)?); @@ -96,11 +100,11 @@ pub fn check_type(env: &Env, t: &IDLType) -> Result { }; Ok(TypeInner::Func(f).into()) } - IDLTypeKind::ServT(ms) => { + IDLType::ServT(ms) => { let ms = check_meths(env, ms)?; Ok(TypeInner::Service(ms).into()) } - IDLTypeKind::ClassT(_, _) => Err(Error::msg("service constructor not supported")), + IDLType::ClassT(_, _) => Err(Error::msg("service constructor not supported")), } } @@ -136,9 +140,17 @@ fn check_meths(env: &Env, ms: &[Binding]) -> Result> { fn check_defs(env: &mut Env, decs: &[Dec]) -> Result<()> { for dec in decs.iter() { - if let Dec::TypD(Binding { id, typ, .. }) = dec { - let t = check_type(env, typ)?; - env.te.0.insert(id.to_string(), t); + match dec { + Dec::TypD(Binding { + id, + typ, + docs: _, + span: _, + }) => { + let t = check_type(env, typ)?; + env.te.0.insert(id.to_string(), t); + } + Dec::ImportType { .. } | Dec::ImportServ { .. } => (), } } Ok(()) @@ -187,20 +199,22 @@ fn check_decs(env: &mut Env, decs: &[Dec]) -> Result<()> { fn check_actor(env: &Env, actor: &Option) -> Result> { match actor.as_ref().map(|a| &a.typ) { None => Ok(None), - Some(IDLTypeKind::ClassT(ts, t)) => { - let mut args = Vec::new(); - for arg in ts.iter() { - args.push(check_type(env, arg)?); + Some(typ) => match &typ.kind { + IDLType::ClassT(ts, t) => { + let mut args = Vec::new(); + for arg in ts.iter() { + args.push(check_type(env, arg)?); + } + let serv = check_type(env, t)?; + env.te.as_service(&serv)?; + Ok(Some(TypeInner::Class(args, serv).into())) } - let serv = check_type(env, t)?; - env.te.as_service(&serv)?; - Ok(Some(TypeInner::Class(args, serv).into())) - } - Some(typ) => { - let t = check_type(env, typ)?; - env.te.as_service(&t)?; - Ok(Some(t)) - } + _ => { + let t = check_type(env, typ)?; + env.te.as_service(&t)?; + Ok(Some(t)) + } + }, } } @@ -222,11 +236,8 @@ fn load_imports( list: &mut Vec<(PathBuf, String, IDLProg)>, ) -> Result<()> { for dec in prog.decs.iter() { - if let Some((file, include_serv)) = match dec { - Dec::ImportType(file) => Some((file, false)), - Dec::ImportServ(file) => Some((file, true)), - _ => None, - } { + let include_serv = matches!(dec, Dec::ImportServ { .. }); + if let Dec::ImportType { path: file, .. } | Dec::ImportServ { path: file, .. } = dec { let path = resolve_path(base, file); match visited.get_mut(&path) { Some(x) => *x = *x || include_serv, diff --git a/rust/candid_parser/tests/parse_type.rs b/rust/candid_parser/tests/parse_type.rs index c1cc83434..de146e10d 100644 --- a/rust/candid_parser/tests/parse_type.rs +++ b/rust/candid_parser/tests/parse_type.rs @@ -2,7 +2,7 @@ use candid::types::TypeEnv; use candid_parser::{ bindings::{javascript, motoko, rust, typescript}, configs::Configs, - syntax::{pretty_print, Dec, IDLProg, IDLTypeKind}, + syntax::{pretty_print, Dec, IDLProg, IDLType}, typing::{check_file, check_prog}, }; use goldenfile::Mint; @@ -42,9 +42,7 @@ service server : { let actor = ast.actor.unwrap(); assert_eq!(actor.docs, vec!["Doc comment for service"]); - let methods = if let IDLTypeKind::ServT(methods) = &actor.typ { - methods - } else { + let IDLType::ServT(methods) = &actor.typ.kind else { panic!("actor is not a service"); }; assert_eq!(methods[0].docs, vec!["Doc comment for f"]); @@ -84,11 +82,9 @@ service server : { } }) .unwrap(); - match &list.typ { - IDLTypeKind::OptT(list_inner) => { - let fields = if let IDLTypeKind::RecordT(fields) = list_inner.as_ref() { - fields - } else { + match &list.typ.kind { + IDLType::OptT(list_inner) => { + let IDLType::RecordT(fields) = &list_inner.kind else { panic!("inner is not a record"); }; assert_eq!(fields[0].docs, vec!["Doc comment for List.head"]); diff --git a/rust/candid_parser/tests/test_doc_comments.rs b/rust/candid_parser/tests/test_doc_comments.rs index c90b783bc..ee714eaf0 100644 --- a/rust/candid_parser/tests/test_doc_comments.rs +++ b/rust/candid_parser/tests/test_doc_comments.rs @@ -1,4 +1,4 @@ -use candid_parser::syntax::{Binding, Dec, IDLProg, IDLType, IDLTypeKind, TypeField}; +use candid_parser::syntax::{Binding, Dec, IDLProg, IDLType, TypeField}; #[test] fn test_doc_comments_separated_by_blank_line() { @@ -41,7 +41,7 @@ type network = variant { assert_eq!(binding.id, "network"); // No field should have the inline comment - let fields = extract_variant_fields(&binding.typ); + let fields = extract_variant_fields(&binding.typ.kind); assert_eq!(fields.len(), 3); for field in fields { assert!( @@ -78,7 +78,7 @@ type network = variant { ); assert_eq!(binding.docs[0], "Doc comment for network"); - let fields = extract_variant_fields(&binding.typ); + let fields = extract_variant_fields(&binding.typ.kind); assert_eq!(fields.len(), 3); // Check that only the testnet field has the doc comment @@ -143,7 +143,7 @@ type my_type = variant { assert_eq!(binding.id, "my_type"); assert_eq!(binding.docs, vec!["Doc comment for my_type"]); - let fields = extract_variant_fields(&binding.typ); + let fields = extract_variant_fields(&binding.typ.kind); assert_eq!(fields.len(), 3); for field in fields { assert!( @@ -162,7 +162,7 @@ fn extract_type_declaration(dec: &Dec) -> &Binding { } fn extract_variant_fields(typ: &IDLType) -> &[TypeField] { - if let IDLTypeKind::VariantT(fields) = typ { + if let IDLType::VariantT(fields) = typ { return fields; } panic!("Expected a variant type"); diff --git a/tools/didc/src/main.rs b/tools/didc/src/main.rs index f22ce6ca8..d77aaa441 100644 --- a/tools/didc/src/main.rs +++ b/tools/didc/src/main.rs @@ -131,7 +131,7 @@ impl TypeAnnotation { (Some(tys), None) => { let mut types = Vec::new(); for ty in tys.args.iter() { - types.push(ast_to_type(&env, ty)?); + types.push(ast_to_type(&env, &ty.kind)?); } Ok((env, types)) }