on master help sign in
Allow single-quoted Text literals
09c2766iko
SPEC.md+3−1
@@ -173,7 +173,9 @@ same value if they have the same field names with equal values.
173- **Type names.** `[A-Z][A-Za-z0-9_]*`. Declared as shapes in `config.j`;
173- **Type names.** `[A-Z][A-Za-z0-9_]*`. Declared as shapes in `config.j`;
174 usable in expressions as values (§4.12).
174 usable in expressions as values (§4.12).
175- **Integer literals.** `[0-9]+`. There is no unary minus; write `0 - 1`.
175- **Integer literals.** `[0-9]+`. There is no unary minus; write `0 - 1`.
176−- **Text literals.** `"…"` with escapes `\"`, `\\`, `\n`, `\t`, `\r`.
176+- **Text literals.** `"…"` or `'…'` with escapes `\"`, `\'`, `\\`, `\n`,
177+ `\t`, `\r`. The closing quote matches the opening one, so a single-quoted
178+ literal may contain unescaped `"` and a double-quoted one unescaped `'`.
177- **Id literals.** `@` immediately followed by one or more of the letters
179- **Id literals.** `@` immediately followed by one or more of the letters
178 `k`–`z` (jj's change-id alphabet), e.g. `@wqzt`: an existing id, given in
180 `k`–`z` (jj's change-id alphabet), e.g. `@wqzt`: an existing id, given in
179 full or by a unique prefix.
181 full or by a unique prefix.
src/lex.rs+9−7
@@ -265,8 +265,9 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
265 push!(Tok::LabelLit(s), start_line, start_col);
265 push!(Tok::LabelLit(s), start_line, start_col);
266 continue;
266 continue;
267 }
267 }
268− // text literal
268+ // text literal, double- or single-quoted
269− if c == '"' {
269+ if c == '"' || c == '\'' {
270+ let quote = c;
270 i += 1;
271 i += 1;
271 col += 1;
272 col += 1;
272 let mut s = String::new();
273 let mut s = String::new();
@@ -275,12 +276,12 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
275 err!("unterminated text literal");
276 err!("unterminated text literal");
276 }
277 }
277 let d = chars[i];
278 let d = chars[i];
279+ if d == quote {
280+ i += 1;
281+ col += 1;
282+ break;
283+ }
278 match d {
284 match d {
279− '"' => {
280− i += 1;
281− col += 1;
282− break;
283− }
284 '\\' => {
285 '\\' => {
285 if i + 1 >= chars.len() {
286 if i + 1 >= chars.len() {
286 err!("unterminated text literal");
287 err!("unterminated text literal");
@@ -288,6 +289,7 @@ pub fn lex(src: &str) -> Result<Vec<SpTok>, LexError> {
288 let e = chars[i + 1];
289 let e = chars[i + 1];
289 let r = match e {
290 let r = match e {
290 '"' => '"',
291 '"' => '"',
292+ '\'' => '\'',
291 '\\' => '\\',
293 '\\' => '\\',
292 'n' => '\n',
294 'n' => '\n',
293 't' => '\t',
295 't' => '\t',
tests/eval.rs+10−1
@@ -227,8 +227,17 @@ fn label_literals_are_revsets() {
227}
227}
228
228
229#[test]
229#[test]
230−fn path_literals_are_text_lists() {
230+fn single_quoted_text() {
231 let (mut i, cfg) = make_interp();
231 let (mut i, cfg) = make_interp();
232+ check!(i, cfg, "'hello'", Value::text("hello"));
233+ check!(i, cfg, "'say \"hi\"'", Value::text("say \"hi\""));
234+ check!(i, cfg, "\"don't\"", Value::text("don't"));
235+ check!(i, cfg, "'a' ++ 'b'", Value::text("ab"));
236+ check!(i, cfg, "'x' == \"x\"", Value::Bool(true));
237+}
238+
239+#[test]
240+fn path_literals_are_text_lists() { let (mut i, cfg) = make_interp();
232 check!(
241 check!(
233 i,
242 i,
234 cfg,
243 cfg,
tests/lex.rs+27
@@ -71,6 +71,33 @@ fn text_literals_and_escapes() {
71 assert!(err(r#""\x""#).contains("escape"));
71 assert!(err(r#""\x""#).contains("escape"));
72}
72}
73
73
74+#[test]
75+fn single_quoted_text_literals() {
76+ // single quotes are an alternative delimiter with the same escapes
77+ assert_eq!(toks("'hello'"), vec![Tok::Text("hello".into())]);
78+ // an unescaped " is fine inside single quotes
79+ assert_eq!(toks(r#"'say "hi"'"#), vec![Tok::Text("say \"hi\"".into())]);
80+ // an unescaped ' is fine inside double quotes
81+ assert_eq!(toks("\"don't\""), vec![Tok::Text("don't".into())]);
82+ // \' escapes the single quote
83+ assert_eq!(toks(r"'don\'t'"), vec![Tok::Text("don't".into())]);
84+ // \" works in single-quoted literals too
85+ assert_eq!(toks(r#"'a\"b'"#), vec![Tok::Text("a\"b".into())]);
86+ // other escapes work the same
87+ assert_eq!(toks(r"'\n\t\r\\'"), vec![Tok::Text("\n\t\r\\".into())]);
88+ // empty
89+ assert_eq!(toks("''"), vec![Tok::Text("".into())]);
90+ // errors
91+ assert!(err("'unterminated").contains("unterminated"));
92+ assert!(err("'a\nb'").contains("newline"));
93+ assert!(err(r"'\x'").contains("escape"));
94+ // adjacent strings are two tokens (application), not one string
95+ assert_eq!(
96+ toks("'it' 's'"),
97+ vec![Tok::Text("it".into()), Tok::Text("s".into())]
98+ );
99+}
100+
74#[test]
101#[test]
75fn id_literals() {
102fn id_literals() {
76 assert_eq!(toks("@wqzt"), vec![Tok::IdLit("wqzt".into())]);
103 assert_eq!(toks("@wqzt"), vec![Tok::IdLit("wqzt".into())]);