Skip to content

Commit 162cd8b

Browse files
olwangclaude
andcommitted
feat(lang): character-literal values (SH-016) + spec
Make '\''x'\'' a real Char value end to end. The lexer emits TokenKind::Char (raw inner text), the parser produces Expr::CharLiteral / MatchLiteral::Char, HIR gains HirExpr::Char typed Char, the reg-VM lowers a new RegInstr::LoadChar (VmValue::Char), and the AOT backend emits a Rust char literal via format!("{:?}", decode_char_token(..)) with no .to_string() (char is Copy). Native keeps LoadChar as native_subset:false, so char-using functions stay on the interpreter tier (safe parity fallback). Char is now an allowed match scrutinee type (infinite domain, requires _). Removes the SH-016 diagnostic scaffolding (Program.char_literal_spans, the analyzer HashSet, the RS0015 char-literal emission). Mirrors the new tokenization in selfhost/scan.rss (Char token kind 9, \-escape honored) so lexer parity holds. Adds decode_char_token, a pass fixture, a differential exec corpus pair, and a vm-eval parity test covering all escapes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c8b36c5 commit 162cd8b

63 files changed

Lines changed: 422 additions & 124 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

crates/rsscript/src/analyzer.rs

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,6 @@ fn analyze_program(
208208
type_alias_params,
209209
in_task_group: false,
210210
async_let_names: Vec::new(),
211-
char_literal_spans: HashSet::new(),
212211
};
213212
analyzer.run();
214213
let mut diagnostics = analyzer.diagnostics;
@@ -275,7 +274,6 @@ fn analyze_syntax_program(
275274
type_alias_params: Default::default(),
276275
in_task_group: false,
277276
async_let_names: Vec::new(),
278-
char_literal_spans: HashSet::new(),
279277
};
280278
analyzer.run_syntax_only();
281279
let mut diagnostics = analyzer.diagnostics;
@@ -311,11 +309,6 @@ pub(crate) struct Analyzer<'a> {
311309
pub(crate) type_alias_params: std::collections::BTreeMap<String, Vec<String>>,
312310
in_task_group: bool,
313311
pub(crate) async_let_names: Vec<String>,
314-
/// Spans of folded `'...'` character-literal tokens. These get one targeted
315-
/// "no character-literal syntax" diagnostic; the generic `Expr::Unknown`
316-
/// walk skips them so the lone `'` operand does not also emit a duplicate
317-
/// RS0015.
318-
pub(crate) char_literal_spans: HashSet<crate::diagnostic::Span>,
319312
}
320313

321314
fn collect_task_group_async_lets(
@@ -586,6 +579,7 @@ where
586579
Expr::Ident(_, _)
587580
| Expr::Number(_, _)
588581
| Expr::String(_, _)
582+
| Expr::CharLiteral(_, _)
589583
| Expr::MultilineString(_, _)
590584
| Expr::Unknown(_) => {}
591585
}
@@ -974,6 +968,7 @@ fn find_nested_task_group_await_span_expr<'a>(
974968
Expr::Ident(_, _)
975969
| Expr::Number(_, _)
976970
| Expr::String(_, _)
971+
| Expr::CharLiteral(_, _)
977972
| Expr::MultilineString(_, _)
978973
| Expr::Unknown(_) => None,
979974
}
@@ -1467,6 +1462,7 @@ fn expr_first_cancellation_token(expr: &Expr) -> Option<crate::diagnostic::Span>
14671462
| Expr::Ident(..)
14681463
| Expr::Number(..)
14691464
| Expr::String(..)
1465+
| Expr::CharLiteral(..)
14701466
| Expr::MultilineString(..)
14711467
| Expr::Unknown(_) => None,
14721468
}
@@ -1534,6 +1530,7 @@ fn expr_first_await(expr: &Expr) -> Option<crate::diagnostic::Span> {
15341530
Expr::Ident(..)
15351531
| Expr::Number(..)
15361532
| Expr::String(..)
1533+
| Expr::CharLiteral(..)
15371534
| Expr::MultilineString(..)
15381535
| Expr::Unknown(_) => None,
15391536
}
@@ -1816,6 +1813,7 @@ fn hir_expr_type_name(expr: &HirExpr) -> Option<&str> {
18161813
HirExpr::Field { access, .. } => access.type_name.as_deref(),
18171814
HirExpr::Number { value, .. } => Some(crate::hir::number_literal_type_name(value)),
18181815
HirExpr::String { .. } => Some("String"),
1816+
HirExpr::Char { .. } => Some("Char"),
18191817
HirExpr::Binary { .. } | HirExpr::Index { .. } => None,
18201818
HirExpr::ObjectLiteral { .. }
18211819
| HirExpr::ArrayLiteral { .. }
@@ -1909,7 +1907,7 @@ fn callee_display(callee: &Callee) -> String {
19091907
fn analyzer_expr_label(expr: &Expr) -> String {
19101908
match expr {
19111909
Expr::Ident(name, _) => name.clone(),
1912-
Expr::String(value, _) | Expr::MultilineString(value, _) => format!("{value:?}"),
1910+
Expr::String(value, _) | Expr::CharLiteral(value, _) | Expr::MultilineString(value, _) => format!("{value:?}"),
19131911
Expr::Field { base, name, .. } => format!("{}.{}", analyzer_expr_label(base), name),
19141912
Expr::Index { base, .. } => format!("{}[]", analyzer_expr_label(base)),
19151913
Expr::Call { callee, .. } => format!("{}()", callee_display(callee)),

crates/rsscript/src/analyzer/assign.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,7 @@ impl<'a> AssignChecker<'a> {
255255
| Expr::Ident(..)
256256
| Expr::Number(..)
257257
| Expr::String(..)
258+
| Expr::CharLiteral(..)
258259
| Expr::MultilineString(..)
259260
| Expr::Unknown(_) => {}
260261
}

crates/rsscript/src/analyzer/exhaustiveness.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,7 @@ impl Analyzer<'_> {
587587
| HirExpr::Ident { .. }
588588
| HirExpr::Number { .. }
589589
| HirExpr::String { .. }
590+
| HirExpr::Char { .. }
590591
| HirExpr::Unknown(_) => {}
591592
}
592593
}

crates/rsscript/src/analyzer/resource_types.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,7 @@ impl Analyzer<'_> {
348348
| Expr::Ident(_, _)
349349
| Expr::Number(_, _)
350350
| Expr::String(_, _)
351+
| Expr::CharLiteral(_, _)
351352
| Expr::MultilineString(_, _)
352353
| Expr::Unknown(_) => {}
353354
}
@@ -481,6 +482,7 @@ impl Analyzer<'_> {
481482
| Expr::Ident(_, _)
482483
| Expr::Number(_, _)
483484
| Expr::String(_, _)
485+
| Expr::CharLiteral(_, _)
484486
| Expr::MultilineString(_, _)
485487
| Expr::Unknown(_) => {}
486488
}

crates/rsscript/src/analyzer/runtime_guarantee.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,7 @@ impl Analyzer<'_> {
249249
| Expr::Ident(_, _)
250250
| Expr::Number(_, _)
251251
| Expr::String(_, _)
252+
| Expr::CharLiteral(_, _)
252253
| Expr::MultilineString(_, _)
253254
| Expr::Unknown(_) => {}
254255
}

crates/rsscript/src/analyzer/syntax_support.rs

Lines changed: 6 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,6 @@ use super::*;
22

33
impl Analyzer<'_> {
44
pub(super) fn check_unsupported_syntax(&mut self) {
5-
// A `'...'` character-literal attempt lexes to one `Symbol("'")` token.
6-
// Report it once with a clear message and remember its span so the
7-
// generic `Expr::Unknown` walk below does not emit a duplicate RS0015 for
8-
// the same lone `'` operand.
9-
let char_literal_spans = self.syntax_program.char_literal_spans.clone();
10-
self.char_literal_spans = char_literal_spans.iter().cloned().collect();
11-
for span in char_literal_spans {
12-
self.unsupported_syntax(
13-
span,
14-
"character literal",
15-
"RSScript has no character-literal syntax. Use a String literal (\"x\") or compare code points with Char.to_code(...).",
16-
);
17-
}
185
for span in self.syntax_program.unknown_top_level_spans.clone() {
196
self.unsupported_syntax(
207
span,
@@ -502,18 +489,14 @@ impl Analyzer<'_> {
502489
Expr::Ident(_, _)
503490
| Expr::Number(_, _)
504491
| Expr::String(_, _)
492+
| Expr::CharLiteral(_, _)
505493
| Expr::MultilineString(_, _) => {}
506494
Expr::Unknown(span) => {
507-
// A lone `'` operand from a character-literal attempt already got
508-
// the targeted "character literal" diagnostic; do not also emit
509-
// the generic unsupported-expression RS0015 for it.
510-
if !self.char_literal_spans.contains(span) {
511-
self.unsupported_syntax(
512-
span.clone(),
513-
"unsupported expression",
514-
"This expression is outside the current RSScript parser surface.",
515-
);
516-
}
495+
self.unsupported_syntax(
496+
span.clone(),
497+
"unsupported expression",
498+
"This expression is outside the current RSScript parser surface.",
499+
);
517500
}
518501
}
519502
}

crates/rsscript/src/analyzer/unknowns.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,7 @@ impl Analyzer<'_> {
318318
| HirExpr::ArrayLiteral { .. }
319319
| HirExpr::Number { .. }
320320
| HirExpr::String { .. }
321+
| HirExpr::Char { .. }
321322
| HirExpr::Unknown(_) => {}
322323
}
323324
}

crates/rsscript/src/checks/body/async_checks.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ pub(super) fn check_await_placement_expr(
157157
HirExpr::Ident { .. }
158158
| HirExpr::Number { .. }
159159
| HirExpr::String { .. }
160+
| HirExpr::Char { .. }
160161
| HirExpr::Unknown(_) => {}
161162
}
162163
}
@@ -262,6 +263,7 @@ pub(super) fn expr_span(expr: &HirExpr) -> &Span {
262263
HirExpr::Ident { span, .. }
263264
| HirExpr::Number { span, .. }
264265
| HirExpr::String { span, .. }
266+
| HirExpr::Char { span, .. }
265267
| HirExpr::ObjectLiteral { span, .. }
266268
| HirExpr::MapLiteral { span, .. }
267269
| HirExpr::ArrayLiteral { span, .. }

crates/rsscript/src/checks/body/binding.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -356,7 +356,7 @@ pub(super) fn collect_expr_uses(expr: &HirExpr, uses: &mut HashSet<String>) {
356356
collect_expr_uses(item, uses);
357357
}
358358
}
359-
HirExpr::Number { .. } | HirExpr::String { .. } | HirExpr::Unknown(_) => {}
359+
HirExpr::Number { .. } | HirExpr::String { .. } | HirExpr::Char { .. } | HirExpr::Unknown(_) => {}
360360
}
361361
}
362362

@@ -416,7 +416,7 @@ pub(super) fn collect_await_operand_live_uses(expr: &HirExpr, uses: &mut HashSet
416416
collect_await_operand_live_uses(item, uses);
417417
}
418418
}
419-
HirExpr::Number { .. } | HirExpr::String { .. } | HirExpr::Unknown(_) => {}
419+
HirExpr::Number { .. } | HirExpr::String { .. } | HirExpr::Char { .. } | HirExpr::Unknown(_) => {}
420420
}
421421
}
422422

crates/rsscript/src/checks/body/closure_captures.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,7 @@ pub(super) fn check_explicit_closure_captures_expr(
149149
HirExpr::Ident { .. }
150150
| HirExpr::Number { .. }
151151
| HirExpr::String { .. }
152+
| HirExpr::Char { .. }
152153
| HirExpr::Unknown(_) => {}
153154
}
154155
}
@@ -479,6 +480,7 @@ pub(super) fn collect_syntax_expr_bindings(expr: &Expr, names: &mut HashSet<Stri
479480
Expr::Ident(_, _)
480481
| Expr::Number(_, _)
481482
| Expr::String(_, _)
483+
| Expr::CharLiteral(_, _)
482484
| Expr::MultilineString(_, _)
483485
| Expr::Unknown(_) => {}
484486
}
@@ -744,6 +746,7 @@ pub(super) fn check_explicit_closure_capture_effects_syntax_expr(
744746
Expr::Ident(_, _)
745747
| Expr::Number(_, _)
746748
| Expr::String(_, _)
749+
| Expr::CharLiteral(_, _)
747750
| Expr::MultilineString(_, _)
748751
| Expr::Unknown(_) => {}
749752
}
@@ -912,6 +915,7 @@ pub(super) fn collect_syntax_capture_effects_expr(
912915
}
913916
Expr::Number(_, _)
914917
| Expr::String(_, _)
918+
| Expr::CharLiteral(_, _)
915919
| Expr::MultilineString(_, _)
916920
| Expr::Unknown(_) => {}
917921
}

0 commit comments

Comments
 (0)