Skip to content

Commit ac08cc3

Browse files
committed
Enhance type checking: Introduce is keyword for type checks, update parser and type checker, and add documentation examples.
1 parent b89b538 commit ac08cc3

8 files changed

Lines changed: 167 additions & 5 deletions

File tree

.spec/language/syntax.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -484,6 +484,7 @@ func attack(target: enemy::Enemy) {
484484
| `..` | String concatenation |
485485
| `...` | Spread |
486486
| `as` | Type casting |
487+
| `is` | Type check → `bool` |
487488
| `? :` | Ternary |
488489

489490
### Ranges
@@ -525,13 +526,31 @@ The method is dispatched at runtime. If no matching method is found, a runtime e
525526

526527
---
527528

528-
## 16. Type Casting
529+
## 16. Type Casting and Type Checking
530+
531+
### Casting (`as`)
529532

530533
```writ
531534
let x = someValue as float
532535
let n = health as int
533536
```
534537

538+
### Type checking (`is`)
539+
540+
`expr is TypeName` evaluates to `bool`. Valid in any expression position, including `if` conditions:
541+
542+
```writ
543+
if entity is Player {
544+
entity.takeDamage(10.0)
545+
}
546+
547+
let isEnemy = obj is Enemy
548+
let either = a is Player || a is NPC
549+
550+
// Namespace-qualified types are supported
551+
let check = obj is enemy::Enemy
552+
```
553+
535554
---
536555

537556
## 17. Comments

crates/writ-lsp/src/references.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -280,7 +280,9 @@ fn collect_from_expr(
280280
}
281281
}
282282
}
283-
ExprKind::ErrorPropagate(inner) | ExprKind::Cast { expr: inner, .. } => {
283+
ExprKind::ErrorPropagate(inner)
284+
| ExprKind::Cast { expr: inner, .. }
285+
| ExprKind::TypeCheck { expr: inner, .. } => {
284286
collect_from_expr(inner, name, uri, locations);
285287
}
286288
ExprKind::StringInterpolation(segments) => {

crates/writ-lsp/src/rename.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -270,7 +270,9 @@ fn collect_from_expr(
270270
}
271271
}
272272
}
273-
ExprKind::ErrorPropagate(inner) | ExprKind::Cast { expr: inner, .. } => {
273+
ExprKind::ErrorPropagate(inner)
274+
| ExprKind::Cast { expr: inner, .. }
275+
| ExprKind::TypeCheck { expr: inner, .. } => {
274276
collect_from_expr(inner, old_name, new_name, edits);
275277
}
276278
ExprKind::StringInterpolation(segments) => {

docs/src/content/docs/language/control-flow.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,19 @@ if health <= 0 {
1515
}
1616
```
1717

18+
### Type checking with `is`
19+
20+
Use `expr is TypeName` as a boolean condition:
21+
22+
```writ
23+
if entity is Player {
24+
entity.takeDamage(10.0)
25+
}
26+
27+
let isEnemy = obj is Enemy
28+
let either = a is Player || a is NPC
29+
```
30+
1831
---
1932

2033
## Ternary
5.08 KB
Binary file not shown.

src/parser/ast.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,13 @@ pub enum ExprKind {
106106
target_type: TypeExpr,
107107
},
108108

109+
/// Type check: `expr is Type` — evaluates to `bool`.
110+
/// Type may be simple (`Player`) or namespace-qualified (`enemy::Enemy`).
111+
TypeCheck {
112+
expr: Box<Expr>,
113+
target_type: TypeExpr,
114+
},
115+
109116
/// String interpolation: sequence of literal and expression segments.
110117
StringInterpolation(Vec<InterpolationSegment>),
111118

src/parser/parser.rs

Lines changed: 112 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ fn infix_precedence(kind: &TokenKind) -> Option<Precedence> {
4242
TokenKind::QuestionQuestion => Some(Precedence::NullCoalesce),
4343
TokenKind::Plus | TokenKind::Minus => Some(Precedence::Addition),
4444
TokenKind::Star | TokenKind::Slash | TokenKind::Percent => Some(Precedence::Multiplication),
45-
TokenKind::As => Some(Precedence::Cast),
45+
TokenKind::As | TokenKind::Is => Some(Precedence::Cast),
4646
TokenKind::Dot
4747
| TokenKind::QuestionDot
4848
| TokenKind::LeftParen
@@ -316,6 +316,34 @@ impl Parser {
316316
Ok(left)
317317
}
318318

319+
/// Like `parse_expr` but does not cross newline boundaries.
320+
/// Used for single-expression `when` arm bodies to prevent the Pratt loop
321+
/// from consuming the `is` keyword of the next arm as an infix type-check.
322+
fn parse_expr_line(&mut self) -> Result<Expr, ParseError> {
323+
let mut left = self.parse_prefix()?;
324+
325+
loop {
326+
if matches!(
327+
self.peek(),
328+
TokenKind::Newline | TokenKind::Semicolon | TokenKind::Eof | TokenKind::RightBrace
329+
) {
330+
break;
331+
}
332+
333+
let Some(prec) = infix_precedence(self.peek()) else {
334+
break;
335+
};
336+
337+
if prec <= Precedence::Lowest {
338+
break;
339+
}
340+
341+
left = self.parse_infix(left, prec)?;
342+
}
343+
344+
Ok(left)
345+
}
346+
319347
// --- Prefix parsing (atoms + unary) ---
320348

321349
fn parse_prefix(&mut self) -> Result<Expr, ParseError> {
@@ -616,6 +644,18 @@ impl Parser {
616644
})
617645
}
618646

647+
// Type check: expr is Type
648+
TokenKind::Is => {
649+
let target_type = self.parse_type_expr()?;
650+
Ok(Expr {
651+
kind: ExprKind::TypeCheck {
652+
expr: Box::new(left),
653+
target_type,
654+
},
655+
span,
656+
})
657+
}
658+
619659
// Call: callee(arg1, name: arg2, ...)
620660
TokenKind::LeftParen => {
621661
self.skip_newlines();
@@ -1251,7 +1291,7 @@ impl Parser {
12511291
let body = if self.peek() == &TokenKind::LeftBrace {
12521292
WhenBody::Block(self.parse_block()?)
12531293
} else {
1254-
WhenBody::Expr(self.parse_expr()?)
1294+
WhenBody::Expr(self.parse_expr_line()?)
12551295
};
12561296

12571297
self.consume_stmt_terminator();
@@ -2164,4 +2204,74 @@ mod tests {
21642204
panic!("expected func decl");
21652205
}
21662206
}
2207+
2208+
#[test]
2209+
fn parse_is_expression_simple() {
2210+
let tokens = Lexer::new("let x = obj is Player").tokenize().unwrap();
2211+
let mut parser = Parser::new(tokens);
2212+
let stmts = parser.parse_program().unwrap();
2213+
if let StmtKind::Let { initializer, .. } = &stmts[0].kind {
2214+
assert!(matches!(
2215+
&initializer.kind,
2216+
ExprKind::TypeCheck {
2217+
target_type: TypeExpr::Simple(name),
2218+
..
2219+
} if name == "Player"
2220+
));
2221+
} else {
2222+
panic!("expected let stmt");
2223+
}
2224+
}
2225+
2226+
#[test]
2227+
fn parse_is_expression_qualified() {
2228+
let tokens = Lexer::new("let x = obj is enemy::Enemy")
2229+
.tokenize()
2230+
.unwrap();
2231+
let mut parser = Parser::new(tokens);
2232+
let stmts = parser.parse_program().unwrap();
2233+
if let StmtKind::Let { initializer, .. } = &stmts[0].kind {
2234+
assert!(matches!(
2235+
&initializer.kind,
2236+
ExprKind::TypeCheck {
2237+
target_type: TypeExpr::Qualified { namespace, name },
2238+
..
2239+
} if namespace == "enemy" && name == "Enemy"
2240+
));
2241+
} else {
2242+
panic!("expected let stmt");
2243+
}
2244+
}
2245+
2246+
#[test]
2247+
fn when_type_match_two_arms_no_infix_consumed() {
2248+
let src = "when result {\n is Success(v) => print(v)\n is Error(e) => print(e)\n}";
2249+
let tokens = Lexer::new(src).tokenize().unwrap();
2250+
let mut parser = Parser::new(tokens);
2251+
let stmts = parser.parse_program().unwrap();
2252+
if let StmtKind::When { arms, .. } = &stmts[0].kind {
2253+
assert_eq!(arms.len(), 2);
2254+
// Second arm must be a TypeMatch, not consumed by first arm's body expression
2255+
assert!(
2256+
matches!(arms[1].pattern, WhenPattern::TypeMatch { .. }),
2257+
"second arm pattern was {:?}, expected TypeMatch",
2258+
arms[1].pattern
2259+
);
2260+
} else {
2261+
panic!("expected when stmt");
2262+
}
2263+
}
2264+
2265+
#[test]
2266+
fn type_check_expr_is_still_valid() {
2267+
// `obj is Player` must still parse as a TypeCheck expression (not broken by the fix)
2268+
let tokens = Lexer::new("let x = obj is Player").tokenize().unwrap();
2269+
let mut parser = Parser::new(tokens);
2270+
let stmts = parser.parse_program().unwrap();
2271+
if let StmtKind::Let { initializer, .. } = &stmts[0].kind {
2272+
assert!(matches!(initializer.kind, ExprKind::TypeCheck { .. }));
2273+
} else {
2274+
panic!("expected let stmt");
2275+
}
2276+
}
21672277
}

src/types/checker/exprs.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,15 @@ impl TypeChecker {
121121
}
122122
}
123123

124+
ExprKind::TypeCheck {
125+
expr: inner,
126+
target_type,
127+
} => {
128+
self.infer_expr(inner)?;
129+
self.resolve_type_expr(target_type, &expr.span)?;
130+
Ok(Type::Bool)
131+
}
132+
124133
ExprKind::Call { callee, args } => self.infer_call(callee, args, &expr.span),
125134

126135
ExprKind::Lambda { params, body } => self.infer_lambda(params, body, &expr.span),

0 commit comments

Comments
 (0)