From f084165eabbb8ad1b8e8969078fce89070ab4d96 Mon Sep 17 00:00:00 2001 From: Laurenz Date: Wed, 24 Feb 2021 21:29:32 +0100 Subject: [PATCH] =?UTF-8?q?While=20loops=20=F0=9F=94=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/eval/mod.rs | 58 +++++++--- src/eval/value.rs | 6 +- src/parse/mod.rs | 22 +++- src/pretty.rs | 13 ++- src/syntax/expr.rs | 39 ++++--- src/syntax/visit.rs | 6 ++ tests/ref/control/for-invalid.png | Bin 1653 -> 0 bytes tests/ref/control/for-value.png | Bin 1702 -> 0 bytes tests/ref/control/for.png | Bin 1389 -> 2877 bytes tests/ref/control/if-invalid.png | Bin 1343 -> 0 bytes tests/ref/control/if.png | Bin 1227 -> 1681 bytes tests/ref/control/invalid.png | Bin 0 -> 3743 bytes tests/ref/control/let-invalid.png | Bin 364 -> 0 bytes .../control/{let-terminated.png => let.png} | Bin tests/ref/control/while.png | Bin 0 -> 838 bytes tests/ref/spacing.png | Bin 3858 -> 5117 bytes tests/typ/control/for-invalid.typ | 32 ------ tests/typ/control/for-value.typ | 20 ---- tests/typ/control/for.typ | 54 +++++++--- tests/typ/control/if-invalid.typ | 28 ----- tests/typ/control/if-value.typ | 21 ---- tests/typ/control/if.typ | 46 ++++++-- tests/typ/control/invalid.typ | 100 ++++++++++++++++++ tests/typ/control/let-invalid.typ | 20 ---- tests/typ/control/let-terminated.typ | 28 ----- tests/typ/control/let.typ | 32 +++++- tests/typ/control/while.typ | 46 ++++++++ tests/typ/spacing.typ | 10 +- tools/test-helper/extension.js | 5 +- 29 files changed, 381 insertions(+), 205 deletions(-) delete mode 100644 tests/ref/control/for-invalid.png delete mode 100644 tests/ref/control/for-value.png delete mode 100644 tests/ref/control/if-invalid.png create mode 100644 tests/ref/control/invalid.png delete mode 100644 tests/ref/control/let-invalid.png rename tests/ref/control/{let-terminated.png => let.png} (100%) create mode 100644 tests/ref/control/while.png delete mode 100644 tests/typ/control/for-invalid.typ delete mode 100644 tests/typ/control/for-value.typ delete mode 100644 tests/typ/control/if-invalid.typ delete mode 100644 tests/typ/control/if-value.typ create mode 100644 tests/typ/control/invalid.typ delete mode 100644 tests/typ/control/let-invalid.typ delete mode 100644 tests/typ/control/let-terminated.typ create mode 100644 tests/typ/control/while.typ diff --git a/src/eval/mod.rs b/src/eval/mod.rs index 596ceb501..3cf978606 100644 --- a/src/eval/mod.rs +++ b/src/eval/mod.rs @@ -118,6 +118,7 @@ impl Eval for Expr { Self::Binary(v) => v.eval(ctx), Self::Let(v) => v.eval(ctx), Self::If(v) => v.eval(ctx), + Self::While(v) => v.eval(ctx), Self::For(v) => v.eval(ctx), } } @@ -403,24 +404,56 @@ impl Eval for ExprIf { fn eval(&self, ctx: &mut EvalContext) -> Self::Output { let condition = self.condition.eval(ctx); - - if let Value::Bool(boolean) = condition { - return if boolean { + if let Value::Bool(condition) = condition { + if condition { self.if_body.eval(ctx) } else if let Some(expr) = &self.else_body { expr.eval(ctx) } else { Value::None - }; - } else if condition != Value::Error { - ctx.diag(error!( - self.condition.span(), - "expected boolean, found {}", - condition.type_name(), - )); + } + } else { + if condition != Value::Error { + ctx.diag(error!( + self.condition.span(), + "expected boolean, found {}", + condition.type_name(), + )); + } + Value::Error } + } +} - Value::Error +impl Eval for ExprWhile { + type Output = Value; + + fn eval(&self, ctx: &mut EvalContext) -> Self::Output { + let mut output = vec![]; + loop { + let condition = self.condition.eval(ctx); + if let Value::Bool(condition) = condition { + if condition { + match self.body.eval(ctx) { + Value::Template(v) => output.extend(v), + Value::Str(v) => output.push(TemplateNode::Str(v)), + Value::Error => return Value::Error, + _ => {} + } + } else { + return Value::Template(output); + } + } else { + if condition != Value::Error { + ctx.diag(error!( + self.condition.span(), + "expected boolean, found {}", + condition.type_name(), + )); + } + return Value::Error; + } + } } } @@ -438,7 +471,8 @@ impl Eval for ExprFor { $(ctx.scopes.def_mut($binding.as_str(), $value);)* match self.body.eval(ctx) { - Value::Template(new) => output.extend(new), + Value::Template(v) => output.extend(v), + Value::Str(v) => output.push(TemplateNode::Str(v)), Value::Error => { ctx.scopes.pop(); return Value::Error; diff --git a/src/eval/value.rs b/src/eval/value.rs index 2a91cf8a8..d910155a1 100644 --- a/src/eval/value.rs +++ b/src/eval/value.rs @@ -586,7 +586,11 @@ primitive! { Color: "color", Value::Color } primitive! { String: "string", Value::Str } primitive! { ValueArray: "array", Value::Array } primitive! { ValueDict: "dictionary", Value::Dict } -primitive! { ValueTemplate: "template", Value::Template } +primitive! { + ValueTemplate: "template", + Value::Template, + Value::Str(v) => vec![TemplateNode::Str(v)], +} primitive! { ValueFunc: "function", Value::Func } primitive! { ValueArgs: "arguments", Value::Args } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 8d6958cf8..d4cf90c75 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -71,7 +71,7 @@ fn node(p: &mut Parser, at_start: &mut bool) -> Option { Token::UnicodeEscape(t) => Node::Text(unicode_escape(p, t)), // Hashtag + keyword / identifier. - Token::Ident(_) | Token::Let | Token::If | Token::For => { + Token::Ident(_) | Token::Let | Token::If | Token::While | Token::For => { *at_start = false; let stmt = token == Token::Let; let group = if stmt { Group::Stmt } else { Group::Expr }; @@ -191,6 +191,7 @@ fn primary(p: &mut Parser) -> Option { // Keywords. Some(Token::Let) => expr_let(p), Some(Token::If) => expr_if(p), + Some(Token::While) => expr_while(p), Some(Token::For) => expr_for(p), // Structures. @@ -382,6 +383,25 @@ fn expr_if(p: &mut Parser) -> Option { expr_if } +/// Parse a while expresion. +fn expr_while(p: &mut Parser) -> Option { + let start = p.start(); + p.assert(Token::While); + + let mut expr_while = None; + if let Some(condition) = expr(p) { + if let Some(body) = body(p) { + expr_while = Some(Expr::While(ExprWhile { + span: p.span(start), + condition: Box::new(condition), + body: Box::new(body), + })); + } + } + + expr_while +} + /// Parse a for expression. fn expr_for(p: &mut Parser) -> Option { let start = p.start(); diff --git a/src/pretty.rs b/src/pretty.rs index de910b99a..0899824ac 100644 --- a/src/pretty.rs +++ b/src/pretty.rs @@ -221,6 +221,7 @@ impl Pretty for Expr { Self::Call(v) => v.pretty(p), Self::Let(v) => v.pretty(p), Self::If(v) => v.pretty(p), + Self::While(v) => v.pretty(p), Self::For(v) => v.pretty(p), } } @@ -413,6 +414,15 @@ impl Pretty for ExprIf { } } +impl Pretty for ExprWhile { + fn pretty(&self, p: &mut Printer) { + p.push_str("while "); + self.condition.pretty(p); + p.push(' '); + self.body.pretty(p); + } +} + impl Pretty for ExprFor { fn pretty(&self, p: &mut Printer) { p.push_str("for "); @@ -718,9 +728,10 @@ mod tests { // Keywords. roundtrip("#let x = 1 + 2"); + test_parse("#if x [y] #else [z]", "#if x [y] else [z]"); + roundtrip("#while x {y}"); roundtrip("#for x in y {z}"); roundtrip("#for k, x in y {z}"); - test_parse("#if x [y] #else [z]", "#if x [y] else [z]"); } #[test] diff --git a/src/syntax/expr.rs b/src/syntax/expr.rs index 5b37bb568..638d9dd31 100644 --- a/src/syntax/expr.rs +++ b/src/syntax/expr.rs @@ -19,19 +19,21 @@ pub enum Expr { Template(ExprTemplate), /// A grouped expression: `(1 + 2)`. Group(ExprGroup), - /// A block expression: `{ #let x = 1; x + 2 }`. + /// A block expression: `{ let x = 1; x + 2 }`. Block(ExprBlock), /// A unary operation: `-x`. Unary(ExprUnary), /// A binary operation: `a + b`. Binary(ExprBinary), - /// An invocation of a function: `foo(...)`, `#[foo ...]`. + /// An invocation of a function: `foo(...)`. Call(ExprCall), - /// A let expression: `#let x = 1`. + /// A let expression: `let x = 1`. Let(ExprLet), - /// An if expression: `#if x { y } #else { z }`. + /// An if expression: `if x { y } else { z }`. If(ExprIf), - /// A for expression: `#for x #in y { z }`. + /// A while expression: `while x { y }`. + While(ExprWhile), + /// A for expression: `for x in y { z }`. For(ExprFor), } @@ -51,6 +53,7 @@ impl Expr { Self::Call(v) => v.span, Self::Let(v) => v.span, Self::If(v) => v.span, + Self::While(v) => v.span, Self::For(v) => v.span, } } @@ -62,6 +65,7 @@ impl Expr { | Expr::Call(_) | Expr::Let(_) | Expr::If(_) + | Expr::While(_) | Expr::For(_) ) } @@ -154,7 +158,7 @@ pub struct ExprGroup { pub expr: Box, } -/// A block expression: `{ #let x = 1; x + 2 }`. +/// A block expression: `{ let x = 1; x + 2 }`. #[derive(Debug, Clone, PartialEq)] pub struct ExprBlock { /// The source code location. @@ -365,7 +369,7 @@ pub enum Associativity { Right, } -/// An invocation of a function: `foo(...)`, `#[foo ...]`. +/// An invocation of a function: `foo(...)`. #[derive(Debug, Clone, PartialEq)] pub struct ExprCall { /// The source code location. @@ -407,7 +411,7 @@ impl ExprArg { } } -/// A let expression: `#let x = 1`. +/// A let expression: `let x = 1`. #[derive(Debug, Clone, PartialEq)] pub struct ExprLet { /// The source code location. @@ -418,7 +422,7 @@ pub struct ExprLet { pub init: Option>, } -/// An if expression: `#if x { y } #else { z }`. +/// An if expression: `if x { y } else { z }`. #[derive(Debug, Clone, PartialEq)] pub struct ExprIf { /// The source code location. @@ -431,7 +435,18 @@ pub struct ExprIf { pub else_body: Option>, } -/// A for expression: `#for x #in y { z }`. +/// A while expression: `while x { y }`. +#[derive(Debug, Clone, PartialEq)] +pub struct ExprWhile { + /// The source code location. + pub span: Span, + /// The condition which selects whether to evaluate the body. + pub condition: Box, + /// The expression to evaluate while the condition is true. + pub body: Box, +} + +/// A for expression: `for x in y { z }`. #[derive(Debug, Clone, PartialEq)] pub struct ExprFor { /// The source code location. @@ -447,9 +462,9 @@ pub struct ExprFor { /// A pattern in a for loop. #[derive(Debug, Clone, PartialEq)] pub enum ForPattern { - /// A value pattern: `#for v #in array`. + /// A value pattern: `for v in array`. Value(Ident), - /// A key-value pattern: `#for k, v #in dict`. + /// A key-value pattern: `for k, v in dict`. KeyValue(Ident, Ident), } diff --git a/src/syntax/visit.rs b/src/syntax/visit.rs index 2d3c683f8..1bf260c76 100644 --- a/src/syntax/visit.rs +++ b/src/syntax/visit.rs @@ -61,6 +61,7 @@ visit! { Expr::Call(e) => v.visit_call(e), Expr::Let(e) => v.visit_let(e), Expr::If(e) => v.visit_if(e), + Expr::While(e) => v.visit_while(e), Expr::For(e) => v.visit_for(e), } } @@ -132,6 +133,11 @@ visit! { } } + fn visit_while(v, node: &ExprWhile) { + v.visit_expr(&node.condition); + v.visit_expr(&node.body); + } + fn visit_for(v, node: &ExprFor) { v.visit_expr(&node.iter); v.visit_expr(&node.body); diff --git a/tests/ref/control/for-invalid.png b/tests/ref/control/for-invalid.png deleted file mode 100644 index d758aa95a55a0ecd8c7b2c26dd6f80130153741c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1653 zcma)7dpOez7@iq^xi8sHWwNFnr?izs7`CCdlSt8Zj_nzt&}EKm42_*stQAH~sUvly zl9^3zQ*LE4MJ$&c<#Jpm#vzA}&R^$`&gFUD=Y8J4-sgM1=X>Ap;(m`kdfMx>K_HMG zndEu^1X6>n=y_m;Kv>tt71qR&T?yV%Jrli(v$t_@je!oea1a8oRiG|Y3&-1Y?5PfB zi1{&m_|4Vc;7z$ot)K>J)as{|(eOb=d?GYmPErOLzWo? z2@rt)FBKNRUj`Pb)4r;}C;x4*tRXoc8J_L8>Xu97>5AkyV+dD0Ww9RBd@p)72QPE- z@m)Z&6uAK1h{QcTVY6+gQS*sol)Rc`m9@t{>iHM#)_P>I{b@AUL{n_#$6v1~%}n65 zAh%kQRX?^LYU=0+Qml2?r0a@a2R*36%dW!;kW6 z5McZ`I}QTeTsp9oDDW_fHJ}kH;~(FtQ;qLT^sjxi#JE5u$@=$9jSNTghm958Iw*3f zg)qAco=A_Wo3jMUfZ{EaZf4PSyp*;iLmJ)wejK1S(QSC-4gHv2KeB9${H!)#kLDU& zVL+3}{~)C3Drd%jdt%0+oQca>Tus{!0i!A2F zM=w9Vfrga`XJjU`eKY+;Dkv;QxAVI*SEw{;+O{>eh*R=*Ka}NMQQkL9oy35yOg~g^ z6`Et3wf%t>cLuAPdHSke<^boVnh!U9$Xa}hZe%lNa2rJJvrv})UiUHNk_FB&#g#G{bHgtnFoErdZvT;@&4ab6; zbqdEO1>t@<@uAPR0jp?^7b#S7I&DI^YZZ|l)gj6-ESCf+I_M#x$9>lgES;DNbTP{w zmfEq;VUyTMxV8D+*F!VoK$fa^3IK2Yw19e;*4?&fArY@T5UczFA`3JwoqVMk3ndL5 z$DYdURKM#8ESwn`Wa=VYK@s=Tv(;K70rNhYB{gqD86yxY>FjYo_k9Mxn3FYV3#fx` zj>31F2yl~b13anf(mu8%U_7NZ$dMegUgTJwX?{D8G?yXhJXF+tc2`jF{OFdwsLW8A ziNI#r8r&Qa``F&Wh4x2pc7hZ4<3y1%V&2m*Xa~pwvqxiq$q;nMGN-mY7Qe@QEkW_x ziIn){s`@}Tv&OPyX(<1R&sZdscvje0dpPq>V|zbrxnrkdC)CN$c3&mB*zMOB-#Xbz zVs5naJ|Ge*5nUOS*M~ZzN+OVt*CSyjQ&ab*f5z_9vpQPLE04q_JMR^Qq z#!#VmH7v;!JJkXNXK)^-;<;`;F*BQ>4FW`S0n-P?}|7$$GW{$Y`6P{|7x&gxU+=C zvDv&)$K}|XwnIh*f^wI@2Ump2XWfml$n%rb3c?X*|7(S~159~79}(8FY|6UMK)M@9 zAvHr3z@-Nh04GK)iRuFh1Khi<7pix^MZe_O^d~W%)0F@K diff --git a/tests/ref/control/for-value.png b/tests/ref/control/for-value.png deleted file mode 100644 index fa323edcd1677a9c5d422a3257d1711a5cd764f2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1702 zcma)-YgEz+8pi(|5YQCC3#Mxd$f%Q-jHvaJfCvH&nQ2ZrY7UmEC5eWTiFw0FvqYRR zFC});bhgpT%9YX_8MCm9VpFWQknmb+Rty7`v+L|R^L2L5`JMMX-=1@RAD*`~BAj7m z;bZ{-fE9~Liv$1=WG(l=*ABo_#c2Ql%4N|)_8cEunw-pN^0zaassarHgaS(%lnv!W z{fobXE?M%{G7enQh_q9T!Y=*esKxm42TPNw%Q>~ae@D-U+TvVQT;cuduBdr1!qd}W?Och;+6PC zZ#d-6syOEnx^jpM5C5oOcT3tw{5#DaYH;-qy^Y)gyDTl~9=<`o0=zPk7Jv+Q)^l0w_uHfHjq3b|La%V8*d z8`;|x%jS-Ea)Y)*wdeaf5fj@`C!+Xyrk4l}&S;U{s_GAgAC2#wa)Yp<=?BLIx%}2W2!%8g-N>;-Q|!|8J{nL z22^h=%n|XgV>r&N_gtY>EJO-E1b3{4KI)L5%hPplI_<3(;xaO=rocv$)o_V>022_( zr0xezDTrU83bGwI1C)_5^Hx?mP;=-EMcfyXc?_1S^#Y=e|J2RMur&liS&N|c;&>-X znp#G!Nxb5vWp&QG#rKGjbvVhb?lZHx+Zh2`JXEo=X%E1qujBWB+_5a9l z8=m`8{BZT1#}(FKM3{7MoHD?tYYBnhWnbiewg>_SYXMc!=z_e)WYVqM`ay-G1V;E*&CHH9G zBbM*NZ3B-Y)0{d4uz_LDU>0uo;)V^0(I0|ElWpxNmvTz@*2j885o1Gd5vAH)T`C$4 z?V9yrm*kz|@u98cP)t0>fa+G-W+p+DZZo-XG)Y~$)v!@Zqcw@`1(qBmN_1;Y@JRjk zBU&GYh~ygh79GCxO334^M49{D@wWcTWn^UK^UHUOkANMoN)HBZwH%l(%Z)N7#Z-Us z5_`R$cFz&6gqp?_U-v4H7e2|tfyZXaZgvxsmr>4z#*)$u7fIFu@>Y?)}#knG!11kY+H5@#P zti99B#OR26sX?_}Z@VzqeM4%?WT}O(n1ubg=5~w`b5lhV#;>r@o(dG%_RT& zB(=u+@L{V%hZx3zR~ zSGQoh1%EpqH}K9N^3klrI9~Wf9!k%%!)lcI)`fa|pj#5>CCm?iyk&eVa7~s)$$eiL>!1KL_uq^RT?%NA^#|3tGXB>ppx3|LqCcO+SCf zM*XBla-M8XT~O|;;~f^&;;-?N>6n0KF@R9dg?W;wr&Ak+hH8mxod#EKTDo))Cet{3 znx5wjouWhzmv8zF{t&i5r31a5zB95L4f YGms(+N26DI{&+wvdN{3}`fbsF0Dm3v)Bpeg diff --git a/tests/ref/control/for.png b/tests/ref/control/for.png index 2f13985ad4d276b45ce42351a5d085ec76205792..cfbc8d08df49909dd6e2f72fa22a108f4cad9905 100644 GIT binary patch literal 2877 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 K;>xj<_`d=3#*hI3 diff --git a/tests/ref/control/if-invalid.png b/tests/ref/control/if-invalid.png deleted file mode 100644 index 319fbdbdf0b5a34313992db647cc8345d4c35431..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1343 zcmeAS@N?(olHy`uVBq!ia0y~yU}OQZFLMA11}A-^uM7;VY@RNTAr-gY-g#TN*g%5i z!CMBa1F|#tT-g2Ci`bVmPv`SsU&6eOc_*`10H;>s`}PYy=O*vGVLn~r_U?M8A0J%$ zRTZUO8y0DFfl!QuU1W;_iz0_3hm$~)0P)O2VFDtqfgtpBb;b=HVof;YuUzo5N2Ncu z>e;LU2LU@KwIR^Hn^Bpc-wf^407Ov=X;L{t9JEkw@%N`W4 zIaI9dQn4)S`+HdoIe0ZF`p_FCP$1NY*MeQ3Cp6(Pqx$^d{ z;m-}jVu^&A%P z*f%}4=jWaht4D83PI2sS+kWDb=eqE<`qS>UZNL0-%N=H?tiaF=;hAv^3*tP4zHu>J zzQmN_Imw+NL)p2~@bDeR70xR~TX-2h<+QJCTg|Ye+Tn%ao+dF!N1R-qqxA#W!Jvz{wEviJABBZCbrUq_HHm{6zjO7JngM zrI!Yi<|L*ZQT)#At!b#c|0yrmp{mgSnRBjrFW>F(!Qxnu>ZhfqCwVR@Pv|LlGNYfZ z<#XRf(UzjCEXusb`L_0-f>?}X8s{;4vi{L_YM97fv(lox zusous^>H`{TYg%an4a^3;{{cReV?<%nfrNE`+QkyS<_`O?b=yx!DXu)OK&S)ncBRR z;c?HplAh-reLX*4Eam9gK2N2Z*Xh+gPdUTMe9SwqT>T`;WZT!-V|?0V{eEQ@p6^W# zYVEsjAAIiCD0TIWzu+YHTe9C&f?H!{_hs-^GpyKseM@P}9sOGeudTA4E&o|QSJ;!S z`>FV(g>z1%uy;Rwk@ED+*CQE?K5q}6`Bf_ycgJ=9hyBjK)c)tZN_SOw`H8(R6r&zk?8 z21$zPz>v1!D=wlAsd+xI$LUdoZ3oe-6tGf#PqG5Jzs2!#TchNV~g~V@uI=Y47TURj;lq)-+zYay(I@?f47fJ<+dAN}ldb&1mTCp6yXo z#C<*YxkpiT{15q@mFv=Sd#RSx~rZ-N$EHIwd_&C#4-!m{ZssATkX^>bP0l+XkK DkJLiJ diff --git a/tests/ref/control/if.png b/tests/ref/control/if.png index 75a20d000574802ddaf901bf527c66e6c20532b3..7db3a8aec3110379b56352aa8de0b66498348073 100644 GIT binary patch literal 1681 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 diff --git a/tests/ref/control/invalid.png b/tests/ref/control/invalid.png new file mode 100644 index 0000000000000000000000000000000000000000..9a119088305660d35a2d2f28790d9bbcc19e2059 GIT binary patch literal 3743 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 literal 0 HcmV?d00001 diff --git a/tests/ref/spacing.png b/tests/ref/spacing.png index 5c3acf9b2a950fefbdea50b4a6a018ee0aaef935..fb102e66f184a7dc427883fcbe0157e2da7472aa 100644 GIT binary patch literal 5117 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 literal 3858 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 2 [{v}]] - ---- -// Block body yields template. -// Should output `[1st, 2nd, 3rd, 4th, 5th, 6th]`. -{ - "[" + for v in (1, 2, 3, 4, 5, 6) { - (if v > 1 [, ] - + [{v}] - + if v == 1 [st] - + if v == 2 [nd] - + if v == 3 [rd] - + if v >= 4 [th]) - } + "]" -} diff --git a/tests/typ/control/for.typ b/tests/typ/control/for.typ index 294345b51..36bce447b 100644 --- a/tests/typ/control/for.typ +++ b/tests/typ/control/for.typ @@ -1,11 +1,10 @@ // Test for loops. -// Ref: false --- +// Empty array. +#for x in () [Nope] + // Array. - -#for x in () {} - #let sum = 0 #for x in (1, 2, 3, 4, 5) { sum += x @@ -13,14 +12,12 @@ #test(sum, 15) ---- -// Dictionary. -// Ref: true -(\ #for k, v in (name: "Typst", age: 2) [ - #h(0.5cm) {k}: {v}, \ -]) +// Dictionary is not traversed in insertion order. +// Should output `age: 1, name: Typst,`. +#for k, v in (name: "Typst", age: 2) [ + {k}: {v}, \ +] ---- // String. { let out = "" @@ -36,6 +33,33 @@ } --- +// Block body. +// Should output `[1st, 2nd, 3rd, 4th, 5th, 6th]`. +{ + "[" + for v in (1, 2, 3, 4, 5, 6) { + (if v > 1 [, ] + + [{v}] + + if v == 1 [st] + + if v == 2 [nd] + + if v == 3 [rd] + + if v >= 4 [th]) + } + "]" +} + +// Template body. +// Should output `234`. +#for v in (1, 2, 3, 4, 5, 6, 7) [#if v >= 2 and v <= 5 { repr(v) }] + +--- +// Value of for loops. +// Ref: false +#test(type(for v in () {}), "template") +#test(type(for v in () []), "template") + +--- +// Error: 14-19 unknown variable +#let error = error + // Uniterable expression. // Error: 11-15 cannot loop over boolean #for v in true {} @@ -44,9 +68,7 @@ // Error: 11-18 cannot add integer and string #for v in 1 + "2" {} -// Error: 14-17 cannot apply '-' to string -#let error = -"" -#let result = for v in (1, 2, 3) { +// A single error stops iteration. +#test(error, for v in (1, 2, 3) { if v < 2 [Ok] else {error} -} -#test(result, error) +}) diff --git a/tests/typ/control/if-invalid.typ b/tests/typ/control/if-invalid.typ deleted file mode 100644 index 6d2deab15..000000000 --- a/tests/typ/control/if-invalid.typ +++ /dev/null @@ -1,28 +0,0 @@ -// Test invalid if syntax. - ---- -// Error: 4 expected expression -#if - -// Error: 4 expected expression -{if} - -// Error: 6 expected body -#if x - -// Error: 1-6 unexpected keyword `else` -#else {} - ---- -// Should output `x`. -// Error: 4 expected expression -#if -x {} - -// Should output `something`. -// Error: 6 expected body -#if x something - -// Should output `A thing.` -// Error: 20 expected body -A#if false {} #else thing diff --git a/tests/typ/control/if-value.typ b/tests/typ/control/if-value.typ deleted file mode 100644 index d7124255f..000000000 --- a/tests/typ/control/if-value.typ +++ /dev/null @@ -1,21 +0,0 @@ -// Test return value of if expressions. -// Ref: false - ---- -{ - let x = 1 - let y = 2 - let z - - // Returns if branch. - z = if x < y { "ok" } - test(z, "ok") - - // Returns else branch. - z = if x > y { "bad" } else { "ok" } - test(z, "ok") - - // Missing else evaluates to none. - z = if x > y { "bad" } - test(z, none) -} diff --git a/tests/typ/control/if.typ b/tests/typ/control/if.typ index 4ed6b6496..8d07e9b82 100644 --- a/tests/typ/control/if.typ +++ b/tests/typ/control/if.typ @@ -3,35 +3,61 @@ --- // Test condition evaluation. #if 1 < 2 [ - Ok. + One. ] #if true == false [ - Bad, but we {dont-care}! + {Bad}, but we {dont-care}! ] --- -// Brace in condition. +// Braced condition. #if {true} [ - Ok. + One. +] + +// Template in condition. +#if [] != none [ + Two. ] // Multi-line condition with parens. #if ( 1 + 1 == 1 -) { - nope -} #else { - "Ok." +) [ + Nope. +] #else { + "Three." } // Multiline. #if false [ Bad. ] #else { - let pt = "." - "Ok" + pt + let point = "." + "Four" + point +} + +--- +// Value of if expressions. +// Ref: false +{ + let x = 1 + let y = 2 + let z + + // Returns if branch. + z = if x < y { "ok" } + test(z, "ok") + + // Returns else branch. + z = if x > y { "bad" } else { "ok" } + test(z, "ok") + + // Missing else evaluates to none. + z = if x > y { "bad" } + test(z, none) } --- diff --git a/tests/typ/control/invalid.typ b/tests/typ/control/invalid.typ new file mode 100644 index 000000000..49158a688 --- /dev/null +++ b/tests/typ/control/invalid.typ @@ -0,0 +1,100 @@ +// Test invalid control syntax. + +--- +// Error: 5 expected identifier +#let + +// Error: 5 expected identifier +{let} + +// Error: 6-9 expected identifier, found string +#let "v" + +// Should output `1`. +// Error: 7 expected semicolon or line break +#let v 1 + +// Error: 9 expected expression +#let v = + +// Should output `= 1`. +// Error: 6-9 expected identifier, found string +#let "v" = 1 + +--- +// Error: 4 expected expression +#if + +// Error: 4 expected expression +{if} + +// Error: 6 expected body +#if x + +// Error: 1-6 unexpected keyword `else` +#else {} + +// Should output `x`. +// Error: 4 expected expression +#if +x {} + +// Should output `something`. +// Error: 6 expected body +#if x something + +// Should output `A thing.` +// Error: 20 expected body +A#if false {} #else thing + +--- +// Error: 7 expected expression +#while + +// Error: 7 expected expression +{while} + +// Error: 9 expected body +#while x + +// Should output `x`. +// Error: 7 expected expression +#while +x {} + +// Should output `something`. +// Error: 9 expected body +#while x something + +--- +// Error: 5 expected identifier +#for + +// Error: 5 expected identifier +{for} + +// Error: 7 expected keyword `in` +#for v + +// Error: 10 expected expression +#for v in + +// Error: 15 expected body +#for v in iter + +// Should output `v in iter`. +// Error: 5 expected identifier +#for +v in iter {} + +// Should output `A thing`. +// Error: 7-10 expected identifier, found string +A#for "v" thing + +// Should output `in iter`. +// Error: 6-9 expected identifier, found string +#for "v" in iter {} + +// Should output `+ b in iter`. +// Error: 7 expected keyword `in` +#for a + b in iter {} diff --git a/tests/typ/control/let-invalid.typ b/tests/typ/control/let-invalid.typ deleted file mode 100644 index f29353edf..000000000 --- a/tests/typ/control/let-invalid.typ +++ /dev/null @@ -1,20 +0,0 @@ -// Test invalid let binding syntax. - ---- -// Error: 5 expected identifier -#let - -// Error: 6-9 expected identifier, found string -#let "v" - -// Should output `1`. -// Error: 7 expected semicolon or line break -#let v 1 - -// Error: 9 expected expression -#let v = - ---- -// Should output `= 1`. -// Error: 6-9 expected identifier, found string -#let "v" = 1 diff --git a/tests/typ/control/let-terminated.typ b/tests/typ/control/let-terminated.typ deleted file mode 100644 index 623265e05..000000000 --- a/tests/typ/control/let-terminated.typ +++ /dev/null @@ -1,28 +0,0 @@ -// Test termination of let statements. - ---- -// Terminated by line break. -#let v1 = 1 -One - -// Terminated by semicolon. -#let v2 = 2; Two - -// Terminated by semicolon and line break. -#let v3 = 3; -Three - -// Terminated because expression ends. -// Error: 12 expected semicolon or line break -#let v4 = 4 Four - -// Terminated by semicolon even though we are in a paren group. -// Error: 2:19 expected expression -// Error: 1:19 expected closing paren -#let v5 = (1, 2 + ; Five - -#test(v1, 1) -#test(v2, 2) -#test(v3, 3) -#test(v4, 4) -#test(v5, (1, 2)) diff --git a/tests/typ/control/let.typ b/tests/typ/control/let.typ index e609d3a9c..8df29b116 100644 --- a/tests/typ/control/let.typ +++ b/tests/typ/control/let.typ @@ -1,7 +1,8 @@ // Test let bindings. -// Ref: false --- +// Ref: false + // Automatically initialized with none. #let x #test(x, none) @@ -9,3 +10,32 @@ // Manually initialized with one. #let x = 1 #test(x, 1) + +--- +// Termination. + +// Terminated by line break. +#let v1 = 1 +One + +// Terminated by semicolon. +#let v2 = 2; Two + +// Terminated by semicolon and line break. +#let v3 = 3; +Three + +// Terminated because expression ends. +// Error: 12 expected semicolon or line break +#let v4 = 4 Four + +// Terminated by semicolon even though we are in a paren group. +// Error: 2:19 expected expression +// Error: 1:19 expected closing paren +#let v5 = (1, 2 + ; Five + +#test(v1, 1) +#test(v2, 2) +#test(v3, 3) +#test(v4, 4) +#test(v5, (1, 2)) diff --git a/tests/typ/control/while.typ b/tests/typ/control/while.typ new file mode 100644 index 000000000..7ad703720 --- /dev/null +++ b/tests/typ/control/while.typ @@ -0,0 +1,46 @@ +// Test while expressions. + +--- +// Should output `2 4 6 8 10`. +#let i = 0 +#while i < 10 [ + { i += 2 } + #i +] + +// Should output `Hi`. +#let iter = true +#while iter { + iter = false + "Hi." +} + +#while false { + dont-care +} + +--- +// Value of while loops. +// Ref: false +#test(type(while false {}), "template") +#test(type(while false []), "template") + +--- +// Error: 14-19 unknown variable +#let error = error + +// Condition must be boolean. +// Error: 8-14 expected boolean, found template +#while [nope] [nope] + +// Make sure that we don't complain twice. +// Error: 8-15 unknown variable +#while nothing {} + +// A single error stops iteration. +#let i = 0 +#test(error, while i < 10 { + i += 1 + if i < 5 [nope] else { error } +}) +#test(i, 5) diff --git a/tests/typ/spacing.typ b/tests/typ/spacing.typ index d44cd84c8..77dac53cc 100644 --- a/tests/typ/spacing.typ +++ b/tests/typ/spacing.typ @@ -19,9 +19,17 @@ A #if true{"B"} C \ A#if false [] #else [B]C \ A#if true [B] #else [] C \ +--- +// Spacing around while loop. + +#let c = true; A#while c [{c = false}B]C \ +#let c = true; A#while c [{c = false}B] C \ +#let c = true; A #while c { c = false; "B" }C \ +#let c = true; A #while c { c = false; "B" } C \ + --- // Spacing around for loop. A#for _ in (none,) [B]C \ A#for _ in (none,) [B] C \ -A #for _ in (none,) [B]C \ +A #for _ in (none,) {"B"}C \ diff --git a/tools/test-helper/extension.js b/tools/test-helper/extension.js index e5325bed8..ad157bcb6 100644 --- a/tools/test-helper/extension.js +++ b/tools/test-helper/extension.js @@ -92,9 +92,9 @@ function getWebviewContent(pngSrc, refSrc, stdout, stderr) {