1use crate::ast::{
36 BeamDecl, BeamRef, ExecutorDecl, ExecutorOptionValue, File, HookDecl, Import, LetBinding,
37 NamedString, Spanned,
38};
39use crate::expr::Expr;
40use crate::lexer::Lexer;
41use crate::template::{StringTemplate, TemplatePart};
42use crate::token::{Span, Token, TokenKind};
43use std::collections::HashSet;
44
45const BEAM_FIELDS: &[&str] = &[
47 "description",
48 "needs",
49 "inputs",
50 "outputs",
51 "run",
52 "env",
53 "cwd",
54 "executor",
55 "allow_failure",
56];
57
58#[derive(Debug, Clone, PartialEq)]
62pub struct ParseError {
63 pub message: String,
64 pub span: Span,
65 pub help: Option<String>,
66}
67
68pub fn parse(source: &str) -> Result<File, ParseError> {
70 let tokens = tokenize(source)?;
71 Parser::new(tokens, source).parse_file()
72}
73
74pub(crate) fn tokenize(source: &str) -> Result<Vec<Token>, ParseError> {
81 let mut tokens = Vec::new();
82 for result in Lexer::new(source) {
83 match result {
84 Ok(token) => {
85 if token.kind != TokenKind::Newline {
86 tokens.push(token);
87 }
88 }
89 Err(err) => {
90 return Err(ParseError {
91 message: err.message,
92 span: err.span,
93 help: None,
94 });
95 }
96 }
97 }
98 Ok(tokens)
99}
100
101#[derive(Default)]
104struct BeamFields {
105 description: Option<StringTemplate>,
106 needs: Vec<Spanned<BeamRef>>,
107 inputs: Vec<StringTemplate>,
108 outputs: Vec<StringTemplate>,
109 run: Vec<StringTemplate>,
110 env: Vec<NamedString>,
111 cwd: Option<StringTemplate>,
112 executor: Option<ExecutorDecl>,
113 allow_failure: bool,
114 seen: HashSet<String>,
115}
116
117pub(crate) struct Parser<'a> {
123 tokens: Vec<Token>,
124 source: &'a str,
125 pos: usize,
126}
127
128impl<'a> Parser<'a> {
129 pub(crate) fn new(tokens: Vec<Token>, source: &'a str) -> Self {
130 Self {
131 tokens,
132 source,
133 pos: 0,
134 }
135 }
136
137 pub(crate) fn source(&self) -> &'a str {
140 self.source
141 }
142
143 pub(crate) fn peek(&self) -> &Token {
144 &self.tokens[self.pos]
145 }
146
147 pub(crate) fn is_eof(&self) -> bool {
148 matches!(self.peek().kind, TokenKind::Eof)
149 }
150
151 pub(crate) fn advance(&mut self) -> Token {
155 let tok = self.tokens[self.pos].clone();
156 if self.pos + 1 < self.tokens.len() {
157 self.pos += 1;
158 }
159 tok
160 }
161
162 pub(crate) fn check(&self, kind: &TokenKind) -> bool {
163 &self.peek().kind == kind
164 }
165
166 pub(crate) fn expect(&mut self, kind: TokenKind, what: &str) -> Result<Token, ParseError> {
167 if self.check(&kind) {
168 Ok(self.advance())
169 } else {
170 Err(self.unexpected(what))
171 }
172 }
173
174 pub(crate) fn unexpected(&self, expected: &str) -> ParseError {
175 let tok = self.peek();
176 ParseError {
177 message: format!("expected {expected}, found {}", describe(&tok.kind)),
178 span: tok.span,
179 help: None,
180 }
181 }
182
183 pub(crate) fn eat_ident(&mut self) -> Result<Spanned<String>, ParseError> {
184 match self.peek().kind.clone() {
185 TokenKind::Ident(name) => {
186 let span = self.advance().span;
187 Ok(Spanned::new(name, span))
188 }
189 _ => Err(self.unexpected("an identifier")),
190 }
191 }
192
193 fn eat_str(&mut self) -> Result<Spanned<String>, ParseError> {
194 match self.peek().kind.clone() {
195 TokenKind::Str(s) => {
196 let span = self.advance().span;
197 Ok(Spanned::new(s, span))
198 }
199 _ => Err(self.unexpected("a string literal")),
200 }
201 }
202
203 pub(crate) fn eat_template(&mut self) -> Result<StringTemplate, ParseError> {
208 if matches!(self.peek().kind, TokenKind::Str(_)) {
209 let tok = self.advance();
210 crate::template::parse_template_at(self.source, tok.span)
211 } else {
212 Err(self.unexpected("a string literal"))
213 }
214 }
215
216 fn parse_bracketed_list<T>(
219 &mut self,
220 mut parse_item: impl FnMut(&mut Self) -> Result<T, ParseError>,
221 ) -> Result<Vec<T>, ParseError> {
222 self.expect(TokenKind::LBracket, "`[`")?;
223 let mut items = Vec::new();
224 if !self.check(&TokenKind::RBracket) {
225 loop {
226 items.push(parse_item(self)?);
227 if self.check(&TokenKind::Comma) {
228 self.advance();
229 if self.check(&TokenKind::RBracket) {
230 break; }
232 } else {
233 break;
234 }
235 }
236 }
237 self.expect(TokenKind::RBracket, "`]`")?;
238 Ok(items)
239 }
240
241 fn parse_template_list(&mut self) -> Result<Vec<StringTemplate>, ParseError> {
242 self.parse_bracketed_list(Self::eat_template)
243 }
244
245 fn parse_beam_ref(&mut self) -> Result<Spanned<BeamRef>, ParseError> {
249 let mut segments = vec![self.eat_ident()?];
250 while self.check(&TokenKind::Colon) {
251 self.advance();
252 segments.push(self.eat_ident()?);
253 }
254
255 let span = Span::new(
256 segments[0].span.start,
257 segments[segments.len() - 1].span.end,
258 );
259 let name = segments.pop().expect("at least one segment was parsed");
260 Ok(Spanned::new(
261 BeamRef {
262 namespace: segments.into_iter().map(|s| s.value).collect(),
263 name: name.value,
264 },
265 span,
266 ))
267 }
268
269 fn parse_needs_list(&mut self) -> Result<Vec<Spanned<BeamRef>>, ParseError> {
270 self.parse_bracketed_list(Self::parse_beam_ref)
271 }
272
273 fn parse_run_value(&mut self) -> Result<Vec<StringTemplate>, ParseError> {
275 if self.check(&TokenKind::LBracket) {
276 self.parse_template_list()
277 } else {
278 Ok(vec![self.eat_template()?])
279 }
280 }
281
282 fn parse_bool_value(&mut self) -> Result<bool, ParseError> {
283 match self.peek().kind {
284 TokenKind::KwTrue => {
285 self.advance();
286 Ok(true)
287 }
288 TokenKind::KwFalse => {
289 self.advance();
290 Ok(false)
291 }
292 _ => Err(self.unexpected("`true` or `false`")),
293 }
294 }
295
296 fn parse_env_value(&mut self) -> Result<StringTemplate, ParseError> {
300 match self.peek().kind.clone() {
301 TokenKind::Str(_) => self.eat_template(),
302 TokenKind::Ident(name) => {
303 let span = self.advance().span;
304 Ok(StringTemplate {
305 parts: vec![TemplatePart::Expr(Expr::Var(Spanned::new(name, span)))],
306 span,
307 })
308 }
309 _ => Err(self.unexpected("a string or an identifier")),
310 }
311 }
312
313 fn parse_env_block(&mut self) -> Result<Vec<NamedString>, ParseError> {
314 self.expect(TokenKind::LBrace, "`{`")?;
315 let mut entries = Vec::new();
316 while !self.check(&TokenKind::RBrace) {
317 let key = self.eat_ident()?;
318 self.expect(TokenKind::Eq, "`=`")?;
319 let value = self.parse_env_value()?;
320 entries.push((key, value));
321 if self.check(&TokenKind::Comma) {
322 self.advance();
323 }
324 }
325 self.expect(TokenKind::RBrace, "`}`")?;
326 Ok(entries)
327 }
328
329 fn parse_executor_decl(&mut self) -> Result<ExecutorDecl, ParseError> {
330 let name = self.eat_ident()?;
331 if !self.check(&TokenKind::LBrace) {
336 return Ok(ExecutorDecl {
337 name,
338 options: Vec::new(),
339 });
340 }
341 self.advance();
342 let mut options = Vec::new();
343 let mut seen = HashSet::new();
350 while !self.check(&TokenKind::RBrace) {
351 let opt_name = self.eat_ident()?;
352 if !seen.insert(opt_name.value.clone()) {
353 return Err(ParseError {
354 message: format!("duplicate executor option `{}`", opt_name.value),
355 span: opt_name.span,
356 help: None,
357 });
358 }
359 let opt_value = match self.peek().kind {
360 TokenKind::KwTrue | TokenKind::KwFalse => {
361 ExecutorOptionValue::Bool(self.parse_bool_value()?)
362 }
363 TokenKind::LBracket => ExecutorOptionValue::List(self.parse_template_list()?),
364 _ => ExecutorOptionValue::Str(self.eat_template()?),
365 };
366 options.push((opt_name, opt_value));
367 }
368 self.expect(TokenKind::RBrace, "`}`")?;
369 Ok(ExecutorDecl { name, options })
370 }
371
372 fn parse_beam_field(&mut self, fields: &mut BeamFields) -> Result<(), ParseError> {
377 let field_tok = self.eat_ident()?;
378 let name = field_tok.value.as_str();
379
380 if !BEAM_FIELDS.contains(&name) {
381 let help = suggest_field(name).map(|f| format!("did you mean `{f}`?"));
382 return Err(ParseError {
383 message: format!("unknown field `{name}`"),
384 span: field_tok.span,
385 help,
386 });
387 }
388
389 if !fields.seen.insert(name.to_string()) {
390 return Err(ParseError {
391 message: format!("duplicate field `{name}`"),
392 span: field_tok.span,
393 help: None,
394 });
395 }
396
397 match name {
398 "description" => fields.description = Some(self.eat_template()?),
399 "needs" => fields.needs = self.parse_needs_list()?,
400 "inputs" => fields.inputs = self.parse_template_list()?,
401 "outputs" => fields.outputs = self.parse_template_list()?,
402 "run" => fields.run = self.parse_run_value()?,
403 "env" => fields.env = self.parse_env_block()?,
404 "cwd" => fields.cwd = Some(self.eat_template()?),
405 "executor" => fields.executor = Some(self.parse_executor_decl()?),
406 "allow_failure" => fields.allow_failure = self.parse_bool_value()?,
407 other => unreachable!("field `{other}` accepted by BEAM_FIELDS but not dispatched"),
408 }
409 Ok(())
410 }
411
412 fn parse_beam(&mut self) -> Result<BeamDecl, ParseError> {
414 let beam_kw = self.expect(TokenKind::KwBeam, "`beam`")?;
415 let name = self.eat_ident()?;
416
417 let mut params = Vec::new();
418 if self.check(&TokenKind::LParen) {
419 self.advance();
420 if !self.check(&TokenKind::RParen) {
421 loop {
422 let param = self.eat_ident()?;
423 reject_reserved(¶m)?;
424 params.push(param);
425 if self.check(&TokenKind::Comma) {
426 self.advance();
427 } else {
428 break;
429 }
430 }
431 }
432 self.expect(TokenKind::RParen, "`)`")?;
433 }
434
435 self.expect(TokenKind::LBrace, "`{`")?;
436 let mut fields = BeamFields::default();
437 while !self.check(&TokenKind::RBrace) {
438 self.parse_beam_field(&mut fields)?;
439 }
440 let rbrace = self.expect(TokenKind::RBrace, "`}`")?;
441
442 Ok(BeamDecl {
443 name,
444 params,
445 description: fields.description,
446 needs: fields.needs,
447 inputs: fields.inputs,
448 outputs: fields.outputs,
449 run: fields.run,
450 env: fields.env,
451 cwd: fields.cwd,
452 executor: fields.executor,
453 allow_failure: fields.allow_failure,
454 span: Span::new(beam_kw.span.start, rbrace.span.end),
455 })
456 }
457
458 fn parse_hook(&mut self) -> Result<HookDecl, ParseError> {
468 let hook_kw = self.expect(TokenKind::KwHook, "`hook`")?;
469 let name = self.eat_ident()?;
470 self.expect(TokenKind::LBrace, "`{`")?;
471 let mut beam = None;
472 while !self.check(&TokenKind::RBrace) {
473 if !self.check(&TokenKind::KwBeam) {
474 let field = self.eat_ident()?;
475 return Err(ParseError {
476 message: format!("unknown hook field `{}`", field.value),
477 span: field.span,
478 help: Some("a hook has exactly one field, `beam`".to_string()),
479 });
480 }
481 let beam_span = self.advance().span;
482 if beam.is_some() {
483 return Err(ParseError {
484 message: "duplicate field `beam`".to_string(),
485 span: beam_span,
486 help: None,
487 });
488 }
489 beam = Some(self.parse_beam_ref()?);
490 }
491 let rbrace = self.expect(TokenKind::RBrace, "`}`")?;
492 let Some(beam) = beam else {
493 return Err(ParseError {
494 message: format!("hook `{}` declares no `beam`", name.value),
495 span: name.span,
496 help: Some("write `beam <name>` inside the block".to_string()),
497 });
498 };
499 Ok(HookDecl {
500 name,
501 beam,
502 span: Span::new(hook_kw.span.start, rbrace.span.end),
503 })
504 }
505
506 fn parse_import(&mut self) -> Result<Import, ParseError> {
508 self.expect(TokenKind::KwImport, "`import`")?;
509 let path = self.eat_str()?;
510 self.expect(TokenKind::KwAs, "`as`")?;
511 let alias = self.eat_ident()?;
512 Ok(Import { path, alias })
513 }
514
515 fn parse_let(&mut self) -> Result<LetBinding, ParseError> {
517 self.expect(TokenKind::KwLet, "`let`")?;
518 let name = self.eat_ident()?;
519 reject_reserved(&name)?;
520 self.expect(TokenKind::Eq, "`=`")?;
521 let value = self.parse_expr()?;
522 Ok(LetBinding { name, value })
523 }
524
525 fn parse_file(&mut self) -> Result<File, ParseError> {
529 let mut version = None;
530 let mut imports = Vec::new();
531 let mut lets = Vec::new();
532 let mut default = None;
533 let mut beams = Vec::new();
534 let mut hooks = Vec::new();
535
536 while !self.is_eof() {
537 match self.peek().kind {
538 TokenKind::KwVersion => {
539 self.advance();
540 version = Some(self.eat_str()?);
541 }
542 TokenKind::KwImport => imports.push(self.parse_import()?),
543 TokenKind::KwLet => lets.push(self.parse_let()?),
544 TokenKind::KwDefault => {
545 self.advance();
546 default = Some(self.eat_ident()?);
547 }
548 TokenKind::KwBeam => beams.push(self.parse_beam()?),
549 TokenKind::KwHook => hooks.push(self.parse_hook()?),
550 _ => {
551 return Err(
552 self.unexpected("`version`, `import`, `let`, `default`, `beam`, or `hook`")
553 );
554 }
555 }
556 }
557
558 let mut seen_beam_names = HashSet::new();
559 for beam in &beams {
560 if !seen_beam_names.insert(beam.name.value.clone()) {
561 return Err(ParseError {
562 message: format!("duplicate beam `{}`", beam.name.value),
563 span: beam.name.span,
564 help: None,
565 });
566 }
567 }
568
569 Ok(File {
570 version,
571 imports,
572 lets,
573 default,
574 beams,
575 hooks,
576 })
577 }
578}
579
580fn reject_reserved(name: &Spanned<String>) -> Result<(), ParseError> {
583 if name.value == "git" {
584 return Err(ParseError {
585 message: "`git` is reserved for the built-in git object".to_string(),
586 span: name.span,
587 help: Some(
588 "pick another name; `git.branch`, `git.sha`, `git.short_sha`, and `git.dirty` \
589 read from it"
590 .to_string(),
591 ),
592 });
593 }
594 Ok(())
595}
596
597pub(crate) const EOF_DESCRIPTION: &str = "end of input";
604
605fn describe(kind: &TokenKind) -> String {
607 match kind {
608 TokenKind::Ident(name) => format!("identifier `{name}`"),
609 TokenKind::Str(_) => "a string literal".to_string(),
610 TokenKind::KwVersion => "`version`".to_string(),
611 TokenKind::KwImport => "`import`".to_string(),
612 TokenKind::KwAs => "`as`".to_string(),
613 TokenKind::KwLet => "`let`".to_string(),
614 TokenKind::KwDefault => "`default`".to_string(),
615 TokenKind::KwBeam => "`beam`".to_string(),
616 TokenKind::KwIf => "`if`".to_string(),
617 TokenKind::KwThen => "`then`".to_string(),
618 TokenKind::KwElse => "`else`".to_string(),
619 TokenKind::KwTrue => "`true`".to_string(),
620 TokenKind::KwFalse => "`false`".to_string(),
621 TokenKind::KwHook => "`hook`".to_string(),
622 TokenKind::LBrace => "`{`".to_string(),
623 TokenKind::RBrace => "`}`".to_string(),
624 TokenKind::LBracket => "`[`".to_string(),
625 TokenKind::RBracket => "`]`".to_string(),
626 TokenKind::LParen => "`(`".to_string(),
627 TokenKind::RParen => "`)`".to_string(),
628 TokenKind::Comma => "`,`".to_string(),
629 TokenKind::Colon => "`:`".to_string(),
630 TokenKind::Eq => "`=`".to_string(),
631 TokenKind::EqEq => "`==`".to_string(),
632 TokenKind::NotEq => "`!=`".to_string(),
633 TokenKind::AndAnd => "`&&`".to_string(),
634 TokenKind::OrOr => "`||`".to_string(),
635 TokenKind::Plus => "`+`".to_string(),
636 TokenKind::Dot => "`.`".to_string(),
637 TokenKind::Newline => "a newline".to_string(),
638 TokenKind::Eof => EOF_DESCRIPTION.to_string(),
639 }
640}
641
642fn suggest_field(name: &str) -> Option<&'static str> {
645 BEAM_FIELDS
646 .iter()
647 .map(|&field| (field, levenshtein(name, field)))
648 .filter(|&(_, distance)| distance <= 2)
649 .min_by_key(|&(_, distance)| distance)
650 .map(|(field, _)| field)
651}
652
653fn levenshtein(a: &str, b: &str) -> usize {
657 let a: Vec<char> = a.chars().collect();
658 let b: Vec<char> = b.chars().collect();
659
660 let mut row: Vec<usize> = (0..=b.len()).collect();
661 for i in 1..=a.len() {
662 let mut prev_diag = row[0];
663 row[0] = i;
664 for j in 1..=b.len() {
665 let temp = row[j];
666 row[j] = if a[i - 1] == b[j - 1] {
667 prev_diag
668 } else {
669 1 + prev_diag.min(row[j]).min(row[j - 1])
670 };
671 prev_diag = temp;
672 }
673 }
674 row[b.len()]
675}
676
677#[cfg(test)]
678mod tests {
679 use crate::parse;
680
681 #[test]
682 fn parses_full_beamfile() {
683 let src = r#"
684version "1"
685import "api/Beamfile" as api
686let profile = env("PROFILE", "debug")
687default build
688
689beam build {
690 description "Compile"
691 needs [api:build, codegen]
692 inputs ["src/**/*.rs"]
693 run "cargo build"
694}
695
696beam deploy(target) {
697 needs [build]
698 allow_failure true
699 run ["echo one", "echo two"]
700}
701"#;
702 let file = parse(src).unwrap();
703 assert_eq!(file.beams.len(), 2);
704 assert_eq!(file.default.as_ref().unwrap().value, "build");
705 let build = &file.beams[0];
706 assert_eq!(build.needs[0].value.namespace, ["api"]);
707 let deploy = &file.beams[1];
708 assert_eq!(deploy.params[0].value, "target");
709 assert!(deploy.allow_failure);
710 assert_eq!(deploy.run.len(), 2);
711 }
712
713 #[test]
714 fn rejects_unknown_beam_field_with_help() {
715 let err = parse("beam x { descriptoin \"typo\" }").unwrap_err();
716 assert!(err.message.contains("unknown field"));
717 assert_eq!(err.help.as_deref(), Some("did you mean `description`?"));
718 }
719
720 #[test]
721 fn rejects_duplicate_beam_names() {
722 let err = parse("beam x { run \"a\" }\nbeam x { run \"b\" }").unwrap_err();
723 assert!(err.message.contains("duplicate beam"));
724 }
725
726 #[test]
727 fn rejects_duplicate_field_in_same_beam() {
728 let err = parse("beam x { run \"a\" run \"b\" }").unwrap_err();
729 assert!(err.message.contains("duplicate field"));
730 }
731
732 #[test]
733 fn needs_list_accepts_trailing_comma_and_newlines() {
734 let file = parse("beam x {\n needs [\n a,\n b,\n ]\n}").unwrap();
735 let needs = &file.beams[0].needs;
736 assert_eq!(needs.len(), 2);
737 assert_eq!(needs[0].value.name, "a");
738 assert_eq!(needs[1].value.name, "b");
739 }
740
741 #[test]
746 fn needs_accepts_a_multi_segment_namespace() {
747 let file = parse("beam x { needs [api:db:migrate] }").unwrap();
748
749 let entry = &file.beams[0].needs[0];
750 assert_eq!(entry.value.namespace, ["api", "db"]);
751 assert_eq!(entry.value.name, "migrate");
752 assert_eq!(
753 &"beam x { needs [api:db:migrate] }"[entry.span.start..entry.span.end],
754 "api:db:migrate"
755 );
756 }
757
758 #[test]
759 fn parses_let_with_equality_expression() {
760 let file = parse(r#"let release = profile == "release""#).unwrap();
761 assert_eq!(file.lets.len(), 1);
762 assert_eq!(file.lets[0].name.value, "release");
763 }
764
765 #[test]
766 fn parses_env_and_executor_blocks() {
767 let file = parse(
768 r#"beam deploy(target) {
769 executor docker { image "deployer:latest" }
770 env { DEPLOY_TARGET = target }
771 run "./scripts/deploy.sh {target}"
772}"#,
773 )
774 .unwrap();
775 let beam = &file.beams[0];
776 let executor = beam.executor.as_ref().unwrap();
777 assert_eq!(executor.name.value, "docker");
778 assert_eq!(executor.options[0].0.value, "image");
779 assert!(matches!(
780 &executor.options[0].1,
781 crate::ast::ExecutorOptionValue::Str(t)
782 if t.parts == vec![crate::TemplatePart::Literal("deployer:latest".to_string())]
783 ));
784 assert_eq!(beam.env[0].0.value, "DEPLOY_TARGET");
785 assert!(matches!(
789 &beam.env[0].1.parts[..],
790 [crate::TemplatePart::Expr(crate::Expr::Var(v))] if v.value == "target"
791 ));
792 }
793
794 #[test]
799 fn executor_without_a_block_parses() {
800 let file = parse("beam b {\n executor system_shell\n run \"x\"\n}").unwrap();
801 let executor = file.beams[0].executor.as_ref().unwrap();
802 assert_eq!(executor.name.value, "system_shell");
803 assert!(executor.options.is_empty());
804 }
805
806 #[test]
807 fn executor_with_a_block_still_parses() {
808 let file = parse("beam b {\n executor docker { image \"i\" }\n run \"x\"\n}").unwrap();
809 let executor = file.beams[0].executor.as_ref().unwrap();
810 assert_eq!(executor.name.value, "docker");
811 assert_eq!(executor.options[0].0.value, "image");
812 }
813
814 #[test]
815 fn reports_unexpected_token_span_not_enclosing_construct() {
816 let err = parse("beam x { needs [a, }").unwrap_err();
817 assert_eq!(err.span, crate::Span::new(19, 20));
819 }
820}