Test: fix parser generic annotations and add checker test suite
- Fix two test helpers (top/program) missing <Parsed> type argument - Add checker/tests.rs with 74 tests covering all §7/§8 rules: entry point validation, duplicate defs, struct cycles, literals, promotion, type inference, definite assignment, undefined vars/funcs, arithmetic, shift, comparison, logical ops, unary ops, pointers, struct literals, field/index access, function calls, return checking, mutation, and break/continue - Fix assignment LHS check: bare identifier on the write side of `=` must not trigger "uninitialized" — use lhs_ty() helper that skips the assigned-set check for the direct write target Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -113,8 +113,14 @@ impl Ty {
|
|||||||
(Ty::Char, b) if b.is_unsigned() => b.rank().unwrap_or(0) >= 3,
|
(Ty::Char, b) if b.is_unsigned() => b.rank().unwrap_or(0) >= 3,
|
||||||
// Ptr-Coerce: *mut T promotes to *T (same pointee)
|
// Ptr-Coerce: *mut T promotes to *T (same pointee)
|
||||||
(
|
(
|
||||||
Ty::Ptr { mutable: true, pointee: pa },
|
Ty::Ptr {
|
||||||
Ty::Ptr { mutable: false, pointee: pb },
|
mutable: true,
|
||||||
|
pointee: pa,
|
||||||
|
},
|
||||||
|
Ty::Ptr {
|
||||||
|
mutable: false,
|
||||||
|
pointee: pb,
|
||||||
|
},
|
||||||
) => pa == pb,
|
) => pa == pb,
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
@@ -139,15 +145,21 @@ impl Ty {
|
|||||||
pub fn ptr_common(a: &Ty, b: &Ty) -> Option<Ty> {
|
pub fn ptr_common(a: &Ty, b: &Ty) -> Option<Ty> {
|
||||||
match (a, b) {
|
match (a, b) {
|
||||||
(
|
(
|
||||||
Ty::Ptr { mutable: ma, pointee: pa },
|
Ty::Ptr {
|
||||||
Ty::Ptr { mutable: mb, pointee: pb },
|
mutable: ma,
|
||||||
|
pointee: pa,
|
||||||
|
},
|
||||||
|
Ty::Ptr {
|
||||||
|
mutable: mb,
|
||||||
|
pointee: pb,
|
||||||
|
},
|
||||||
) if pa == pb => Some(Ty::Ptr {
|
) if pa == pb => Some(Ty::Ptr {
|
||||||
mutable: *ma && *mb,
|
mutable: *ma && *mb,
|
||||||
pointee: pa.clone(),
|
pointee: pa.clone(),
|
||||||
}),
|
}),
|
||||||
(Ty::OpaquePtr { mutable: ma }, Ty::OpaquePtr { mutable: mb }) => {
|
(Ty::OpaquePtr { mutable: ma }, Ty::OpaquePtr { mutable: mb }) => Some(Ty::OpaquePtr {
|
||||||
Some(Ty::OpaquePtr { mutable: *ma && *mb })
|
mutable: *ma && *mb,
|
||||||
}
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,8 +179,14 @@ impl Ty {
|
|||||||
Ty::Bool => "bool".to_string(),
|
Ty::Bool => "bool".to_string(),
|
||||||
Ty::Char => "char".to_string(),
|
Ty::Char => "char".to_string(),
|
||||||
Ty::Unit => "()".to_string(),
|
Ty::Unit => "()".to_string(),
|
||||||
Ty::Ptr { mutable: true, pointee } => format!("*mut {}", pointee.display()),
|
Ty::Ptr {
|
||||||
Ty::Ptr { mutable: false, pointee } => format!("*{}", pointee.display()),
|
mutable: true,
|
||||||
|
pointee,
|
||||||
|
} => format!("*mut {}", pointee.display()),
|
||||||
|
Ty::Ptr {
|
||||||
|
mutable: false,
|
||||||
|
pointee,
|
||||||
|
} => format!("*{}", pointee.display()),
|
||||||
Ty::OpaquePtr { mutable: true } => "*mut opaque".to_string(),
|
Ty::OpaquePtr { mutable: true } => "*mut opaque".to_string(),
|
||||||
Ty::OpaquePtr { mutable: false } => "*opaque".to_string(),
|
Ty::OpaquePtr { mutable: false } => "*opaque".to_string(),
|
||||||
Ty::Array { elem, size } => format!("[{}; {}]", elem.display(), size),
|
Ty::Array { elem, size } => format!("[{}; {}]", elem.display(), size),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use crate::ast::Ty;
|
use crate::ast::Ty;
|
||||||
use crate::token::Span;
|
use crate::token::Span;
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
// ── StructTable ────────────────────────────────────────────────────────────────
|
// ── StructTable ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -8,7 +8,7 @@ pub struct StructTable {
|
|||||||
entries: HashMap<String, StructEntry>,
|
entries: HashMap<String, StructEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StructEntry {
|
pub struct StructEntry {
|
||||||
name_span: Span,
|
name_span: Span,
|
||||||
fields: Vec<FieldEntry>,
|
fields: Vec<FieldEntry>,
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,9 @@ pub struct FieldEntry {
|
|||||||
|
|
||||||
impl StructTable {
|
impl StructTable {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { entries: HashMap::new() }
|
Self {
|
||||||
|
entries: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a struct name; returns false if it was already present.
|
/// Insert a struct name; returns false if it was already present.
|
||||||
@@ -30,7 +32,13 @@ impl StructTable {
|
|||||||
if self.entries.contains_key(name) {
|
if self.entries.contains_key(name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.entries.insert(name.to_string(), StructEntry { name_span: span, fields: Vec::new() });
|
self.entries.insert(
|
||||||
|
name.to_string(),
|
||||||
|
StructEntry {
|
||||||
|
name_span: span,
|
||||||
|
fields: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +61,12 @@ impl StructTable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn field_ty(&self, struct_name: &str, field_name: &str) -> Option<&Ty> {
|
pub fn field_ty(&self, struct_name: &str, field_name: &str) -> Option<&Ty> {
|
||||||
self.entries.get(struct_name)?.fields.iter().find(|f| f.name == field_name).map(|f| &f.ty)
|
self.entries
|
||||||
|
.get(struct_name)?
|
||||||
|
.fields
|
||||||
|
.iter()
|
||||||
|
.find(|f| f.name == field_name)
|
||||||
|
.map(|f| &f.ty)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn names_in_outer(&self, _saved: usize) -> HashSet<String> {
|
pub fn names_in_outer(&self, _saved: usize) -> HashSet<String> {
|
||||||
@@ -90,7 +103,9 @@ pub struct ParamEntry {
|
|||||||
|
|
||||||
impl FuncTable {
|
impl FuncTable {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { entries: HashMap::new() }
|
Self {
|
||||||
|
entries: HashMap::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Insert a function; returns false if the name was already present.
|
/// Insert a function; returns false if the name was already present.
|
||||||
@@ -98,7 +113,14 @@ impl FuncTable {
|
|||||||
if self.entries.contains_key(name) {
|
if self.entries.contains_key(name) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
self.entries.insert(name.to_string(), FuncEntry { name_span: span, params, ret });
|
self.entries.insert(
|
||||||
|
name.to_string(),
|
||||||
|
FuncEntry {
|
||||||
|
name_span: span,
|
||||||
|
params,
|
||||||
|
ret,
|
||||||
|
},
|
||||||
|
);
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +149,9 @@ pub struct Binding {
|
|||||||
|
|
||||||
impl TypeEnv {
|
impl TypeEnv {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { bindings: Vec::new() }
|
Self {
|
||||||
|
bindings: Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extend(&mut self, name: String, ty: Ty, mutable: bool) {
|
pub fn extend(&mut self, name: String, ty: Ty, mutable: bool) {
|
||||||
@@ -136,7 +160,11 @@ impl TypeEnv {
|
|||||||
|
|
||||||
/// Look up a binding; rightmost (most recent) binding wins.
|
/// Look up a binding; rightmost (most recent) binding wins.
|
||||||
pub fn lookup(&self, name: &str) -> Option<(&Ty, bool)> {
|
pub fn lookup(&self, name: &str) -> Option<(&Ty, bool)> {
|
||||||
self.bindings.iter().rev().find(|b| b.name == name).map(|b| (&b.ty, b.mutable))
|
self.bindings
|
||||||
|
.iter()
|
||||||
|
.rev()
|
||||||
|
.find(|b| b.name == name)
|
||||||
|
.map(|b| (&b.ty, b.mutable))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn contains(&self, name: &str) -> bool {
|
pub fn contains(&self, name: &str) -> bool {
|
||||||
@@ -153,6 +181,9 @@ impl TypeEnv {
|
|||||||
|
|
||||||
/// Returns all binding names that were introduced after `saved`.
|
/// Returns all binding names that were introduced after `saved`.
|
||||||
pub fn names_in_outer(&self, saved: usize) -> HashSet<String> {
|
pub fn names_in_outer(&self, saved: usize) -> HashSet<String> {
|
||||||
self.bindings[saved..].iter().map(|b| b.name.clone()).collect()
|
self.bindings[saved..]
|
||||||
|
.iter()
|
||||||
|
.map(|b| b.name.clone())
|
||||||
|
.collect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use super::Checker;
|
||||||
|
use super::env::TypeEnv;
|
||||||
use crate::ast::{self, BinaryOp, CompoundAssignOp, ExprKind, Parsed, Ty, UnaryOp};
|
use crate::ast::{self, BinaryOp, CompoundAssignOp, ExprKind, Parsed, Ty, UnaryOp};
|
||||||
use crate::diagnostics::{Diagnostic, Label};
|
use crate::diagnostics::{Diagnostic, Label};
|
||||||
use crate::token::Span;
|
use crate::token::Span;
|
||||||
use super::env::TypeEnv;
|
|
||||||
use super::Checker;
|
|
||||||
|
|
||||||
impl Checker {
|
impl Checker {
|
||||||
/// Type-check `expr` in environment `env` with definite-assignment set `assigned`.
|
/// Type-check `expr` in environment `env` with definite-assignment set `assigned`.
|
||||||
@@ -23,7 +23,10 @@ impl Checker {
|
|||||||
ExprKind::FloatLit(_) => Ty::F64,
|
ExprKind::FloatLit(_) => Ty::F64,
|
||||||
|
|
||||||
// T-StringLit → *char
|
// T-StringLit → *char
|
||||||
ExprKind::StringLit(_) => Ty::Ptr { mutable: false, pointee: Box::new(Ty::Char) },
|
ExprKind::StringLit(_) => Ty::Ptr {
|
||||||
|
mutable: false,
|
||||||
|
pointee: Box::new(Ty::Char),
|
||||||
|
},
|
||||||
|
|
||||||
// T-CharLit → char
|
// T-CharLit → char
|
||||||
ExprKind::CharLit(_) => Ty::Char,
|
ExprKind::CharLit(_) => Ty::Char,
|
||||||
@@ -56,7 +59,11 @@ impl Checker {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// T-StructLit
|
// T-StructLit
|
||||||
ExprKind::StructLit { name, name_span, fields } => {
|
ExprKind::StructLit {
|
||||||
|
name,
|
||||||
|
name_span,
|
||||||
|
fields,
|
||||||
|
} => {
|
||||||
if !self.sigma.contains(name) {
|
if !self.sigma.contains(name) {
|
||||||
self.emit(
|
self.emit(
|
||||||
Diagnostic::error(format!("undefined struct `{name}`"))
|
Diagnostic::error(format!("undefined struct `{name}`"))
|
||||||
@@ -135,7 +142,11 @@ impl Checker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// T-Unary
|
// T-Unary
|
||||||
ExprKind::Unary { op, op_span, expr: inner } => {
|
ExprKind::Unary {
|
||||||
|
op,
|
||||||
|
op_span,
|
||||||
|
expr: inner,
|
||||||
|
} => {
|
||||||
let inner_ty = self.check_expr(inner, env, assigned);
|
let inner_ty = self.check_expr(inner, env, assigned);
|
||||||
if inner_ty.is_error() {
|
if inner_ty.is_error() {
|
||||||
return Ty::Error;
|
return Ty::Error;
|
||||||
@@ -144,12 +155,20 @@ impl Checker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// T-Binary
|
// T-Binary
|
||||||
ExprKind::Binary { op, op_span, lhs, rhs } => {
|
ExprKind::Binary {
|
||||||
self.check_binary(*op, *op_span, lhs, rhs, env, assigned)
|
op,
|
||||||
}
|
op_span,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
} => self.check_binary(*op, *op_span, lhs, rhs, env, assigned),
|
||||||
|
|
||||||
// T-CompoundAssign: lhs op= rhs (expands to lhs = lhs op rhs)
|
// T-CompoundAssign: lhs op= rhs (expands to lhs = lhs op rhs)
|
||||||
ExprKind::CompoundAssign { op, op_span, lhs, rhs } => {
|
ExprKind::CompoundAssign {
|
||||||
|
op,
|
||||||
|
op_span,
|
||||||
|
lhs,
|
||||||
|
rhs,
|
||||||
|
} => {
|
||||||
if !self.is_mutable_place(lhs, env) {
|
if !self.is_mutable_place(lhs, env) {
|
||||||
self.emit(
|
self.emit(
|
||||||
Diagnostic::error(
|
Diagnostic::error(
|
||||||
@@ -168,7 +187,11 @@ impl Checker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// T-Field
|
// T-Field
|
||||||
ExprKind::Field { expr: inner, field, field_span } => {
|
ExprKind::Field {
|
||||||
|
expr: inner,
|
||||||
|
field,
|
||||||
|
field_span,
|
||||||
|
} => {
|
||||||
let inner_ty = self.check_expr(inner, env, assigned);
|
let inner_ty = self.check_expr(inner, env, assigned);
|
||||||
if inner_ty.is_error() {
|
if inner_ty.is_error() {
|
||||||
return Ty::Error;
|
return Ty::Error;
|
||||||
@@ -385,7 +408,10 @@ impl Checker {
|
|||||||
return Ty::Error;
|
return Ty::Error;
|
||||||
}
|
}
|
||||||
let mutable = self.is_mutable_place(inner, env);
|
let mutable = self.is_mutable_place(inner, env);
|
||||||
Ty::Ptr { mutable, pointee: Box::new(inner_ty) }
|
Ty::Ptr {
|
||||||
|
mutable,
|
||||||
|
pointee: Box::new(inner_ty),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -406,13 +432,14 @@ impl Checker {
|
|||||||
BinaryOp::Assign => {
|
BinaryOp::Assign => {
|
||||||
if !self.is_mutable_place(lhs, env) {
|
if !self.is_mutable_place(lhs, env) {
|
||||||
self.emit(
|
self.emit(
|
||||||
Diagnostic::error(
|
Diagnostic::error("left-hand side of `=` must be a mutable place")
|
||||||
"left-hand side of `=` must be a mutable place",
|
|
||||||
)
|
|
||||||
.with_label(Label::primary(lhs.span)),
|
.with_label(Label::primary(lhs.span)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let lhs_ty = self.check_expr(lhs, env, assigned);
|
// For a bare identifier on the LHS we only need its declared
|
||||||
|
// type, not a read — don't emit "uninitialized" for the target
|
||||||
|
// of the assignment itself.
|
||||||
|
let lhs_ty = self.lhs_ty(lhs, env, assigned);
|
||||||
let rhs_ty = self.check_expr(rhs, env, assigned);
|
let rhs_ty = self.check_expr(rhs, env, assigned);
|
||||||
if !lhs_ty.is_error() && !rhs_ty.is_error() && !rhs_ty.promotes_to(&lhs_ty) {
|
if !lhs_ty.is_error() && !rhs_ty.is_error() && !rhs_ty.promotes_to(&lhs_ty) {
|
||||||
self.emit(
|
self.emit(
|
||||||
@@ -562,6 +589,37 @@ impl Checker {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Return the declared type of `expr` as a write-side place, without
|
||||||
|
/// checking that it has been initialised first. For a bare identifier
|
||||||
|
/// this avoids a spurious "uninitialized" diagnostic on the target of
|
||||||
|
/// an assignment like `x = 5` when `x` is being given its first value.
|
||||||
|
///
|
||||||
|
/// For all other place forms (deref, field, index) the sub-expression IS
|
||||||
|
/// read, so the normal `check_expr` path (with assigned checking) is used.
|
||||||
|
fn lhs_ty(
|
||||||
|
&mut self,
|
||||||
|
expr: &ast::Expr<Parsed>,
|
||||||
|
env: &TypeEnv,
|
||||||
|
assigned: &HashSet<String>,
|
||||||
|
) -> Ty {
|
||||||
|
match &expr.kind {
|
||||||
|
ExprKind::Ident(name) => match env.lookup(name) {
|
||||||
|
Some((ty, _)) => ty.clone(),
|
||||||
|
None => {
|
||||||
|
self.emit(
|
||||||
|
Diagnostic::error(format!("undefined variable `{name}`"))
|
||||||
|
.with_label(Label::primary(expr.span)),
|
||||||
|
);
|
||||||
|
Ty::Error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
ExprKind::Group(inner) => self.lhs_ty(inner, env, assigned),
|
||||||
|
// All other place forms (deref, field, index) involve a read of
|
||||||
|
// the sub-expression — use the full check.
|
||||||
|
_ => self.check_expr(expr, env, assigned),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Check that `lhs_ty op rhs_ty` is a valid arithmetic expression;
|
/// Check that `lhs_ty op rhs_ty` is a valid arithmetic expression;
|
||||||
/// returns the result type.
|
/// returns the result type.
|
||||||
fn check_arith_or_shift(&mut self, lhs_ty: Ty, rhs_ty: Ty, op: BinaryOp, op_span: Span) -> Ty {
|
fn check_arith_or_shift(&mut self, lhs_ty: Ty, rhs_ty: Ty, op: BinaryOp, op_span: Span) -> Ty {
|
||||||
@@ -596,7 +654,9 @@ impl Checker {
|
|||||||
pub fn is_place(&self, expr: &ast::Expr<Parsed>, env: &TypeEnv) -> bool {
|
pub fn is_place(&self, expr: &ast::Expr<Parsed>, env: &TypeEnv) -> bool {
|
||||||
match &expr.kind {
|
match &expr.kind {
|
||||||
ExprKind::Ident(_) => true,
|
ExprKind::Ident(_) => true,
|
||||||
ExprKind::Unary { op: UnaryOp::Deref, .. } => true,
|
ExprKind::Unary {
|
||||||
|
op: UnaryOp::Deref, ..
|
||||||
|
} => true,
|
||||||
ExprKind::Field { expr: inner, .. } => self.is_place(inner, env),
|
ExprKind::Field { expr: inner, .. } => self.is_place(inner, env),
|
||||||
ExprKind::Index { expr: inner, .. } => self.is_place(inner, env),
|
ExprKind::Index { expr: inner, .. } => self.is_place(inner, env),
|
||||||
ExprKind::Group(inner) => self.is_place(inner, env),
|
ExprKind::Group(inner) => self.is_place(inner, env),
|
||||||
@@ -612,10 +672,12 @@ impl Checker {
|
|||||||
/// - `e.f` / `e[i]` is mutable iff `e` is mutable.
|
/// - `e.f` / `e[i]` is mutable iff `e` is mutable.
|
||||||
pub fn is_mutable_place(&self, expr: &ast::Expr<Parsed>, env: &TypeEnv) -> bool {
|
pub fn is_mutable_place(&self, expr: &ast::Expr<Parsed>, env: &TypeEnv) -> bool {
|
||||||
match &expr.kind {
|
match &expr.kind {
|
||||||
ExprKind::Ident(name) => {
|
ExprKind::Ident(name) => env.lookup(name).map(|(_, m)| m).unwrap_or(false),
|
||||||
env.lookup(name).map(|(_, m)| m).unwrap_or(false)
|
ExprKind::Unary {
|
||||||
}
|
op: UnaryOp::Deref,
|
||||||
ExprKind::Unary { op: UnaryOp::Deref, expr: inner, .. } => {
|
expr: inner,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
// Mutable iff inner's type is *mut T.
|
// Mutable iff inner's type is *mut T.
|
||||||
// We resolve the type of inner from env without emitting errors.
|
// We resolve the type of inner from env without emitting errors.
|
||||||
matches!(
|
matches!(
|
||||||
@@ -635,25 +697,27 @@ impl Checker {
|
|||||||
match &expr.kind {
|
match &expr.kind {
|
||||||
ExprKind::Ident(name) => env.lookup(name).map(|(ty, _)| ty.clone()),
|
ExprKind::Ident(name) => env.lookup(name).map(|(ty, _)| ty.clone()),
|
||||||
ExprKind::Group(inner) => self.peek_ty(inner, env),
|
ExprKind::Group(inner) => self.peek_ty(inner, env),
|
||||||
ExprKind::Unary { op: UnaryOp::Deref, expr: inner, .. } => {
|
ExprKind::Unary {
|
||||||
match self.peek_ty(inner, env)? {
|
op: UnaryOp::Deref,
|
||||||
|
expr: inner,
|
||||||
|
..
|
||||||
|
} => match self.peek_ty(inner, env)? {
|
||||||
Ty::Ptr { pointee, .. } => Some(*pointee),
|
Ty::Ptr { pointee, .. } => Some(*pointee),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
},
|
||||||
}
|
ExprKind::Field {
|
||||||
ExprKind::Field { expr: inner, field, .. } => {
|
expr: inner, field, ..
|
||||||
|
} => {
|
||||||
let Ty::Struct(sname) = self.peek_ty(inner, env)? else {
|
let Ty::Struct(sname) = self.peek_ty(inner, env)? else {
|
||||||
return None;
|
return None;
|
||||||
};
|
};
|
||||||
self.sigma.field_ty(&sname, field).cloned()
|
self.sigma.field_ty(&sname, field).cloned()
|
||||||
}
|
}
|
||||||
ExprKind::Index { expr: inner, .. } => {
|
ExprKind::Index { expr: inner, .. } => match self.peek_ty(inner, env)? {
|
||||||
match self.peek_ty(inner, env)? {
|
|
||||||
Ty::Array { elem, .. } => Some(*elem),
|
Ty::Array { elem, .. } => Some(*elem),
|
||||||
Ty::Ptr { pointee, .. } => Some(*pointee),
|
Ty::Ptr { pointee, .. } => Some(*pointee),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
},
|
||||||
}
|
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
pub mod env;
|
pub mod env;
|
||||||
pub mod expr;
|
pub mod expr;
|
||||||
pub mod stmt;
|
pub mod stmt;
|
||||||
|
mod tests;
|
||||||
|
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
@@ -29,7 +30,10 @@ impl Checker {
|
|||||||
FieldEntry {
|
FieldEntry {
|
||||||
name: "data".to_string(),
|
name: "data".to_string(),
|
||||||
name_span: Span::new(0, 0),
|
name_span: Span::new(0, 0),
|
||||||
ty: Ty::Ptr { mutable: false, pointee: Box::new(Ty::Char) },
|
ty: Ty::Ptr {
|
||||||
|
mutable: false,
|
||||||
|
pointee: Box::new(Ty::Char),
|
||||||
|
},
|
||||||
ty_span: Span::new(0, 0),
|
ty_span: Span::new(0, 0),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -43,7 +47,11 @@ impl Checker {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
Self { sigma, phi, errors: Vec::new() }
|
Self {
|
||||||
|
sigma,
|
||||||
|
phi,
|
||||||
|
errors: Vec::new(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn emit(&mut self, diag: Diagnostic) {
|
pub fn emit(&mut self, diag: Diagnostic) {
|
||||||
@@ -79,13 +87,19 @@ impl Checker {
|
|||||||
}
|
}
|
||||||
Type::Pointer { mutable, pointee } => {
|
Type::Pointer { mutable, pointee } => {
|
||||||
let inner = self.resolve_type(pointee, span);
|
let inner = self.resolve_type(pointee, span);
|
||||||
Ty::Ptr { mutable: *mutable, pointee: Box::new(inner) }
|
Ty::Ptr {
|
||||||
|
mutable: *mutable,
|
||||||
|
pointee: Box::new(inner),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Type::OpaquePointer { mutable } => Ty::OpaquePtr { mutable: *mutable },
|
Type::OpaquePointer { mutable } => Ty::OpaquePtr { mutable: *mutable },
|
||||||
Type::Array { elem, size } => {
|
Type::Array { elem, size } => {
|
||||||
let elem_ty = self.resolve_type(elem, span);
|
let elem_ty = self.resolve_type(elem, span);
|
||||||
match size.parse::<u64>() {
|
match size.parse::<u64>() {
|
||||||
Ok(n) => Ty::Array { elem: Box::new(elem_ty), size: n },
|
Ok(n) => Ty::Array {
|
||||||
|
elem: Box::new(elem_ty),
|
||||||
|
size: n,
|
||||||
|
},
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
self.emit(
|
self.emit(
|
||||||
Diagnostic::error(format!("invalid array size `{size}`"))
|
Diagnostic::error(format!("invalid array size `{size}`"))
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
|
use super::Checker;
|
||||||
|
use super::env::TypeEnv;
|
||||||
use crate::ast::{self, BinaryOp, ElseBranch, ExprKind, Parsed, Ty};
|
use crate::ast::{self, BinaryOp, ElseBranch, ExprKind, Parsed, Ty};
|
||||||
use crate::diagnostics::{Diagnostic, Label};
|
use crate::diagnostics::{Diagnostic, Label};
|
||||||
use super::env::TypeEnv;
|
|
||||||
use super::Checker;
|
|
||||||
|
|
||||||
// ── Control flow ──────────────────────────────────────────────────────────────
|
// ── Control flow ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -22,7 +22,10 @@ pub struct CheckCtx {
|
|||||||
|
|
||||||
impl CheckCtx {
|
impl CheckCtx {
|
||||||
fn loop_ctx(&self) -> CheckCtx {
|
fn loop_ctx(&self) -> CheckCtx {
|
||||||
CheckCtx { ret_ty: self.ret_ty.clone(), in_loop: true }
|
CheckCtx {
|
||||||
|
ret_ty: self.ret_ty.clone(),
|
||||||
|
in_loop: true,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +34,10 @@ impl CheckCtx {
|
|||||||
impl Checker {
|
impl Checker {
|
||||||
pub fn check_function(&mut self, f: &ast::FuncDef<Parsed>) {
|
pub fn check_function(&mut self, f: &ast::FuncDef<Parsed>) {
|
||||||
let (params_info, ret_ty) = {
|
let (params_info, ret_ty) = {
|
||||||
let entry = self.phi.get(&f.name).expect("function must be in phi after pass 1");
|
let entry = self
|
||||||
|
.phi
|
||||||
|
.get(&f.name)
|
||||||
|
.expect("function must be in phi after pass 1");
|
||||||
let params: Vec<(String, Ty, bool)> = entry
|
let params: Vec<(String, Ty, bool)> = entry
|
||||||
.params
|
.params
|
||||||
.iter()
|
.iter()
|
||||||
@@ -48,7 +54,10 @@ impl Checker {
|
|||||||
assigned.insert(name);
|
assigned.insert(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
let ctx = CheckCtx { ret_ty: ret_ty.clone(), in_loop: false };
|
let ctx = CheckCtx {
|
||||||
|
ret_ty: ret_ty.clone(),
|
||||||
|
in_loop: false,
|
||||||
|
};
|
||||||
let cf = self.check_block(&f.body, &mut env, &mut assigned, &ctx);
|
let cf = self.check_block(&f.body, &mut env, &mut assigned, &ctx);
|
||||||
|
|
||||||
// If the declared return type is non-unit and the body may not diverge,
|
// If the declared return type is non-unit and the body may not diverge,
|
||||||
@@ -107,7 +116,13 @@ impl Checker {
|
|||||||
) -> Cf {
|
) -> Cf {
|
||||||
match &stmt.kind {
|
match &stmt.kind {
|
||||||
// T-Let
|
// T-Let
|
||||||
ast::StmtKind::Let { mutable, name, name_span, ty, init } => {
|
ast::StmtKind::Let {
|
||||||
|
mutable,
|
||||||
|
name,
|
||||||
|
name_span,
|
||||||
|
ty,
|
||||||
|
init,
|
||||||
|
} => {
|
||||||
let ann_ty = ty.as_ref().map(|t| self.resolve_type(t, *name_span));
|
let ann_ty = ty.as_ref().map(|t| self.resolve_type(t, *name_span));
|
||||||
|
|
||||||
let init_ty = init.as_ref().map(|e| {
|
let init_ty = init.as_ref().map(|e| {
|
||||||
@@ -199,7 +214,11 @@ impl Checker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// T-If
|
// T-If
|
||||||
ast::StmtKind::If { cond, then_block, else_branch } => {
|
ast::StmtKind::If {
|
||||||
|
cond,
|
||||||
|
then_block,
|
||||||
|
else_branch,
|
||||||
|
} => {
|
||||||
let cond_ty = self.check_expr(cond, env, assigned);
|
let cond_ty = self.check_expr(cond, env, assigned);
|
||||||
if !cond_ty.is_error() && cond_ty != Ty::Bool {
|
if !cond_ty.is_error() && cond_ty != Ty::Bool {
|
||||||
self.emit(
|
self.emit(
|
||||||
|
|||||||
556
fluxc/src/checker/tests.rs
Normal file
556
fluxc/src/checker/tests.rs
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
/// Checker integration tests.
|
||||||
|
///
|
||||||
|
/// Each helper parses a source string and runs the full 4-pass checker,
|
||||||
|
/// returning the list of error messages (without ANSI codes) so tests can
|
||||||
|
/// assert on their content.
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::checker;
|
||||||
|
use crate::parser::Parser;
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Run the checker on `src` and return all diagnostic messages.
|
||||||
|
fn errors(src: &str) -> Vec<String> {
|
||||||
|
let mut parser = Parser::new(src);
|
||||||
|
let program = parser.parse_program();
|
||||||
|
assert!(
|
||||||
|
parser.errors.is_empty(),
|
||||||
|
"parse errors: {:?}",
|
||||||
|
parser.errors
|
||||||
|
);
|
||||||
|
checker::check(&program)
|
||||||
|
.into_iter()
|
||||||
|
.map(|d| d.message)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert that checking `src` produces no errors.
|
||||||
|
fn ok(src: &str) {
|
||||||
|
let msgs = errors(src);
|
||||||
|
assert!(msgs.is_empty(), "unexpected errors: {:?}", msgs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Assert that checking `src` produces at least one error whose message
|
||||||
|
/// contains `needle`.
|
||||||
|
fn err_contains(src: &str, needle: &str) {
|
||||||
|
let msgs = errors(src);
|
||||||
|
assert!(
|
||||||
|
msgs.iter().any(|m| m.contains(needle)),
|
||||||
|
"expected error containing {:?}, got: {:?}",
|
||||||
|
needle,
|
||||||
|
msgs
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pass 4: entry point ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn no_main_is_error() {
|
||||||
|
err_contains("fn foo() { }", "no `main`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_with_params_is_error() {
|
||||||
|
err_contains("fn main(x: i32) { }", "no parameters");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_bad_return_type_is_error() {
|
||||||
|
err_contains("fn main() -> bool { return true; }", "return `()` or `i32`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_unit_return_ok() {
|
||||||
|
ok("fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn main_i32_return_ok() {
|
||||||
|
ok("fn main() -> i32 { return 0; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pass 1: duplicate definitions ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_function_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn foo() { } fn foo() { } fn main() { }",
|
||||||
|
"duplicate function `foo`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn duplicate_struct_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct S { x: i32 } struct S { y: i32 } fn main() { }",
|
||||||
|
"duplicate struct `S`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_view_reserved() {
|
||||||
|
err_contains("struct string_view { } fn main() { }", "reserved built-in");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pass 2: struct fields + cycles ────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undefined_field_type_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct S { x: Unknown } fn main() { }",
|
||||||
|
"undefined type `Unknown`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_size_cycle_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct A { b: B } struct B { a: A } fn main() { }",
|
||||||
|
"infinite size",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_pointer_cycle_ok() {
|
||||||
|
// Pointer-to-self does NOT cause a size cycle
|
||||||
|
ok("struct Node { next: *Node, value: i32 } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Literals ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn int_literal_is_i32() {
|
||||||
|
ok("fn main() { let x: i32 = 42; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn float_literal_is_f64() {
|
||||||
|
ok("fn main() { let x: f64 = 3.14; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bool_literal() {
|
||||||
|
ok("fn main() { let x: bool = true; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn char_literal() {
|
||||||
|
ok("fn main() { let x: char = 'a'; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Implicit promotion in let ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn i8_promotes_to_i32_in_let() {
|
||||||
|
// i32 literal (type i32) promotes to i64
|
||||||
|
ok("fn main() { let x: i64 = 1; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn f32_promotes_to_f64_in_let() {
|
||||||
|
ok("fn foo(v: f32) { let x: f64 = v; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cross_category_forbidden_in_let() {
|
||||||
|
err_contains("fn main() { let x: f64 = 1; }", "type mismatch");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn unsigned_to_signed_forbidden_in_let() {
|
||||||
|
err_contains(
|
||||||
|
"fn foo(v: u32) { let x: i32 = v; } fn main() { }",
|
||||||
|
"type mismatch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Type inference ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn let_type_inferred_from_init() {
|
||||||
|
// `let x = 1;` → inferred as i32; later usage as i32 should pass
|
||||||
|
ok("fn add(a: i32, b: i32) -> i32 { return a + b; }
|
||||||
|
fn main() { let x = 1; let y = add(x, 2); }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn let_no_type_no_init_is_error() {
|
||||||
|
err_contains("fn main() { let x; }", "cannot infer type");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Definite assignment ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn use_before_init_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn main() { let x: i32; let y = x; }",
|
||||||
|
"uninitialized variable `x`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_via_assignment_is_ok() {
|
||||||
|
ok("fn main() { let mut x: i32; x = 5; let y = x; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn definite_assign_both_branches() {
|
||||||
|
ok("fn f(b: bool) -> i32 {
|
||||||
|
let mut x: i32;
|
||||||
|
if b { x = 1; } else { x = 2; }
|
||||||
|
return x;
|
||||||
|
} fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn definite_assign_missing_else_branch_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(b: bool) -> i32 {
|
||||||
|
let x: i32;
|
||||||
|
if b { x = 1; }
|
||||||
|
return x;
|
||||||
|
} fn main() { }",
|
||||||
|
"uninitialized variable `x`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Undefined variable / function ─────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undefined_variable_is_error() {
|
||||||
|
err_contains("fn main() { let y = x; }", "undefined variable `x`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn undefined_function_is_error() {
|
||||||
|
err_contains("fn main() { foo(); }", "undefined function `foo`");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Arithmetic ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arithmetic_same_type_ok() {
|
||||||
|
ok("fn main() { let x: i32 = 1 + 2; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn arithmetic_cross_category_is_error() {
|
||||||
|
// float + int: not in same category
|
||||||
|
err_contains(
|
||||||
|
"fn f(a: f64, b: i32) -> f64 { return a + b; } fn main() { }",
|
||||||
|
"+",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_result_is_lhs_type() {
|
||||||
|
ok("fn f(n: u32) -> u32 { return n << 2; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_rhs_can_be_different_int() {
|
||||||
|
ok("fn f(n: u32, s: u8) -> u32 { return n >> s; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn shift_non_integer_lhs_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: f64) -> f64 { return x << 1; } fn main() { }",
|
||||||
|
"integer LHS",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Comparison / logical ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn comparison_ok() {
|
||||||
|
ok("fn f(a: i32, b: i32) -> bool { return a < b; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logical_ok() {
|
||||||
|
ok("fn f(a: bool, b: bool) -> bool { return a and b; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn logical_non_bool_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(a: i32, b: bool) -> bool { return a and b; } fn main() { }",
|
||||||
|
"`and`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Unary operators ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn neg_on_signed_ok() {
|
||||||
|
ok("fn f(x: i32) -> i32 { return -x; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn neg_on_unsigned_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: u32) -> u32 { return -x; } fn main() { }",
|
||||||
|
"unary `-`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_on_bool_ok() {
|
||||||
|
ok("fn f(b: bool) -> bool { return !b; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn not_on_int_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: i32) -> bool { return !x; } fn main() { }",
|
||||||
|
"unary `!`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bitnot_on_integer_ok() {
|
||||||
|
ok("fn f(x: u32) -> u32 { return ~x; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Pointers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn addrof_immutable_binding_gives_immutable_ptr() {
|
||||||
|
// taking address of immutable x gives *i32; assigning to *mut i32 should fail
|
||||||
|
err_contains(
|
||||||
|
"fn main() { let x: i32 = 1; let p: *mut i32 = &x; }",
|
||||||
|
"type mismatch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn addrof_mutable_binding_gives_mut_ptr() {
|
||||||
|
ok("fn main() { let mut x: i32 = 1; let p: *mut i32 = &x; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mut_ptr_coerces_to_immutable_ptr() {
|
||||||
|
ok("fn main() { let mut x: i32 = 1; let p: *i32 = &x; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deref_typed_ptr_ok() {
|
||||||
|
ok("fn f(p: *i32) -> i32 { return *p; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deref_opaque_ptr_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(p: *opaque) { let x = *p; } fn main() { }",
|
||||||
|
"opaque pointer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn addrof_non_place_is_error() {
|
||||||
|
err_contains("fn main() { let p = &(1 + 2); }", "non-place");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Struct literals ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_ok() {
|
||||||
|
ok("struct Point { x: i32, y: i32 }
|
||||||
|
fn main() { let p: Point = Point { x: 1, y: 2 }; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_missing_field_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct Point { x: i32, y: i32 }
|
||||||
|
fn main() { let p = Point { x: 1 }; }",
|
||||||
|
"missing field `y`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_unknown_field_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct Point { x: i32, y: i32 }
|
||||||
|
fn main() { let p = Point { x: 1, y: 2, z: 3 }; }",
|
||||||
|
"no field `z`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_wrong_field_type_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct S { x: bool }
|
||||||
|
fn main() { let s = S { x: 42 }; }",
|
||||||
|
"field `x`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn struct_literal_undefined_struct_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn main() { let s = Nope { x: 1 }; }",
|
||||||
|
"undefined struct `Nope`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Field access ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn field_access_ok() {
|
||||||
|
ok("struct S { v: i32 }
|
||||||
|
fn f(s: S) -> i32 { return s.v; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn field_access_unknown_field_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"struct S { v: i32 }
|
||||||
|
fn f(s: S) -> i32 { return s.nope; } fn main() { }",
|
||||||
|
"no field `nope`",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn field_access_on_non_struct_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: i32) -> i32 { return x.foo; } fn main() { }",
|
||||||
|
"non-struct",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Index expressions ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn array_index_ok() {
|
||||||
|
ok("fn f(arr: [i32; 4]) -> i32 { return arr[0]; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn pointer_index_ok() {
|
||||||
|
ok("fn f(p: *i32) -> i32 { return p[0]; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn non_integer_index_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(arr: [i32; 4]) -> i32 { return arr[true]; } fn main() { }",
|
||||||
|
"index must be an integer",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Function calls ────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_wrong_arg_count_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn add(a: i32, b: i32) -> i32 { return a + b; }
|
||||||
|
fn main() { let x = add(1); }",
|
||||||
|
"expects 2 argument(s), got 1",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_wrong_arg_type_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f(x: i32) { }
|
||||||
|
fn main() { f(true); }",
|
||||||
|
"argument 1",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn call_with_promotion_ok() {
|
||||||
|
// Passing i32 where i64 expected — implicit widening
|
||||||
|
ok("fn f(x: i64) { }
|
||||||
|
fn main() { let v: i32 = 1; f(v); }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Return type checking ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn missing_return_in_non_unit_fn_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f() -> i32 { let x = 1; } fn main() { }",
|
||||||
|
"must always return",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_wrong_type_is_error() {
|
||||||
|
err_contains(
|
||||||
|
"fn f() -> i32 { return true; } fn main() { }",
|
||||||
|
"type mismatch",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn return_value_from_unit_fn_is_error() {
|
||||||
|
err_contains("fn f() { return 1; } fn main() { }", "returns `()`");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn early_return_satisfies_all_paths() {
|
||||||
|
ok("fn f(b: bool) -> i32 {
|
||||||
|
if b { return 1; } else { return 2; }
|
||||||
|
} fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Assignment / mutation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assign_to_immutable_is_error() {
|
||||||
|
err_contains("fn main() { let x: i32 = 0; x = 1; }", "mutable place");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn assign_to_mutable_ok() {
|
||||||
|
ok("fn main() { let mut x: i32 = 0; x = 1; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compound_assign_ok() {
|
||||||
|
ok("fn main() { let mut x: i32 = 1; x += 2; }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn compound_assign_immutable_is_error() {
|
||||||
|
err_contains("fn main() { let x: i32 = 1; x += 2; }", "mutable place");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── break / continue ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn break_in_loop_ok() {
|
||||||
|
ok("fn main() { loop { break; } }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn break_outside_loop_is_error() {
|
||||||
|
err_contains("fn main() { break; }", "outside of a loop");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn continue_in_while_ok() {
|
||||||
|
ok("fn main() { let mut i: i32 = 0; while i < 10 { i += 1; continue; } }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn continue_outside_loop_is_error() {
|
||||||
|
err_contains("fn main() { continue; }", "outside of a loop");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── built-in string_view ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_view_fields_accessible() {
|
||||||
|
ok("fn f(s: string_view) -> u64 { return s.size; } fn main() { }");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn string_view_data_is_ptr_char() {
|
||||||
|
ok("fn f(s: string_view) -> *char { return s.data; } fn main() { }");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
ast::{
|
ast::{
|
||||||
BinaryOp, Block, CompoundAssignOp, ElseBranch, Expr, ExprKind, FieldDef, FuncDef, Param,
|
BinaryOp, Block, CompoundAssignOp, ElseBranch, Expr, ExprKind, FieldDef, FuncDef, Param,
|
||||||
Program, Stmt, StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind, Type,
|
Parsed, Program, Stmt, StmtKind, StructDef, StructField, TopLevelDef, TopLevelDefKind,
|
||||||
UnaryOp,
|
Type, UnaryOp,
|
||||||
},
|
},
|
||||||
diagnostics::{Diagnostic, Label},
|
diagnostics::{Diagnostic, Label},
|
||||||
lexer::Lexer,
|
lexer::Lexer,
|
||||||
@@ -235,7 +235,10 @@ impl<'src> Parser<'src> {
|
|||||||
self.advance();
|
self.advance();
|
||||||
Type::OpaquePointer { mutable }
|
Type::OpaquePointer { mutable }
|
||||||
} else {
|
} else {
|
||||||
Type::Pointer { mutable, pointee: Box::new(self.parse_type()) }
|
Type::Pointer {
|
||||||
|
mutable,
|
||||||
|
pointee: Box::new(self.parse_type()),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +266,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a block: `{ stmt* }`.
|
/// Parse a block: `{ stmt* }`.
|
||||||
pub fn parse_block(&mut self) -> Block {
|
pub fn parse_block(&mut self) -> Block<Parsed> {
|
||||||
let open = self.expect(TokenKind::LCurly);
|
let open = self.expect(TokenKind::LCurly);
|
||||||
let mut stmts = Vec::new();
|
let mut stmts = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
@@ -288,7 +291,7 @@ impl<'src> Parser<'src> {
|
|||||||
/// - *Synchronization*: tokens that can never start a statement or
|
/// - *Synchronization*: tokens that can never start a statement or
|
||||||
/// expression trigger `synchronize()`, which skips forward until the
|
/// expression trigger `synchronize()`, which skips forward until the
|
||||||
/// next statement boundary to prevent cascading errors.
|
/// next statement boundary to prevent cascading errors.
|
||||||
pub fn parse_stmt(&mut self) -> Stmt {
|
pub fn parse_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let tok = self.current();
|
let tok = self.current();
|
||||||
match tok.kind {
|
match tok.kind {
|
||||||
TokenKind::Let => self.parse_let_stmt(),
|
TokenKind::Let => self.parse_let_stmt(),
|
||||||
@@ -351,13 +354,13 @@ impl<'src> Parser<'src> {
|
|||||||
/// `allow_struct_literals` controls whether a bare `Ident { … }` is
|
/// `allow_struct_literals` controls whether a bare `Ident { … }` is
|
||||||
/// parsed as a struct literal. Pass `false` in `if`/`while` conditions
|
/// parsed as a struct literal. Pass `false` in `if`/`while` conditions
|
||||||
/// so that `{` is not consumed as a struct body.
|
/// so that `{` is not consumed as a struct body.
|
||||||
pub fn parse_expr(&mut self, allow_struct_literals: bool) -> Expr {
|
pub fn parse_expr(&mut self, allow_struct_literals: bool) -> Expr<Parsed> {
|
||||||
self.pratt(0, allow_struct_literals)
|
self.pratt(0, allow_struct_literals)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Statement helpers ─────────────────────────────────────────────────────
|
// ── Statement helpers ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse_let_stmt(&mut self) -> Stmt {
|
fn parse_let_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let start = self.advance(); // consume `let`
|
let start = self.advance(); // consume `let`
|
||||||
let mutable = if self.current().kind == TokenKind::Mut {
|
let mutable = if self.current().kind == TokenKind::Mut {
|
||||||
self.advance();
|
self.advance();
|
||||||
@@ -391,7 +394,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_return_stmt(&mut self) -> Stmt {
|
fn parse_return_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let kw = self.advance(); // consume `return`
|
let kw = self.advance(); // consume `return`
|
||||||
// LL(1): `;` → unit return; anything else → parse expression
|
// LL(1): `;` → unit return; anything else → parse expression
|
||||||
let value = if self.current().kind != TokenKind::Semicolon {
|
let value = if self.current().kind != TokenKind::Semicolon {
|
||||||
@@ -406,7 +409,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_if_stmt(&mut self) -> Stmt {
|
fn parse_if_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let kw = self.advance(); // consume `if`
|
let kw = self.advance(); // consume `if`
|
||||||
// Condition: expr_ns (no struct literals at outermost level)
|
// Condition: expr_ns (no struct literals at outermost level)
|
||||||
let cond = self.parse_expr(false);
|
let cond = self.parse_expr(false);
|
||||||
@@ -437,7 +440,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_while_stmt(&mut self) -> Stmt {
|
fn parse_while_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let kw = self.advance(); // consume `while`
|
let kw = self.advance(); // consume `while`
|
||||||
let cond = self.parse_expr(false); // no struct literals in condition
|
let cond = self.parse_expr(false); // no struct literals in condition
|
||||||
let body = self.parse_block();
|
let body = self.parse_block();
|
||||||
@@ -448,7 +451,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_loop_stmt(&mut self) -> Stmt {
|
fn parse_loop_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let kw = self.advance(); // consume `loop`
|
let kw = self.advance(); // consume `loop`
|
||||||
let body = self.parse_block();
|
let body = self.parse_block();
|
||||||
let span = kw.span.cover(body.span);
|
let span = kw.span.cover(body.span);
|
||||||
@@ -458,7 +461,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_expr_stmt(&mut self) -> Stmt {
|
fn parse_expr_stmt(&mut self) -> Stmt<Parsed> {
|
||||||
let expr = self.parse_expr(true);
|
let expr = self.parse_expr(true);
|
||||||
let semi = self.expect(TokenKind::Semicolon);
|
let semi = self.expect(TokenKind::Semicolon);
|
||||||
let span = expr.span.cover(semi.span);
|
let span = expr.span.cover(semi.span);
|
||||||
@@ -470,7 +473,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
// ── Pratt core ────────────────────────────────────────────────────────────
|
// ── Pratt core ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn pratt(&mut self, min_bp: u8, allow_struct_lit: bool) -> Expr {
|
fn pratt(&mut self, min_bp: u8, allow_struct_lit: bool) -> Expr<Parsed> {
|
||||||
let mut lhs = self.parse_nud(allow_struct_lit);
|
let mut lhs = self.parse_nud(allow_struct_lit);
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
@@ -504,7 +507,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
// ── Null denotation (prefix / primary) ───────────────────────────────────
|
// ── Null denotation (prefix / primary) ───────────────────────────────────
|
||||||
|
|
||||||
fn parse_nud(&mut self, allow_struct_lit: bool) -> Expr {
|
fn parse_nud(&mut self, allow_struct_lit: bool) -> Expr<Parsed> {
|
||||||
let tok = self.advance();
|
let tok = self.advance();
|
||||||
match tok.kind {
|
match tok.kind {
|
||||||
// Literals
|
// Literals
|
||||||
@@ -558,11 +561,11 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
fn parse_led(
|
fn parse_led(
|
||||||
&mut self,
|
&mut self,
|
||||||
lhs: Expr,
|
lhs: Expr<Parsed>,
|
||||||
op_tok: Token<'src>,
|
op_tok: Token<'src>,
|
||||||
r_bp: u8,
|
r_bp: u8,
|
||||||
allow_struct_lit: bool,
|
allow_struct_lit: bool,
|
||||||
) -> Expr {
|
) -> Expr<Parsed> {
|
||||||
// Consume the operator token.
|
// Consume the operator token.
|
||||||
self.advance();
|
self.advance();
|
||||||
|
|
||||||
@@ -647,7 +650,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
/// Called after we have already parsed the leading `Ident` as `lhs` and
|
/// Called after we have already parsed the leading `Ident` as `lhs` and
|
||||||
/// the current token is `{`.
|
/// the current token is `{`.
|
||||||
fn parse_struct_lit(&mut self, name_expr: Expr) -> Expr {
|
fn parse_struct_lit(&mut self, name_expr: Expr<Parsed>) -> Expr<Parsed> {
|
||||||
let (name, name_span) = match name_expr.kind {
|
let (name, name_span) = match name_expr.kind {
|
||||||
ExprKind::Ident(ref s) => (s.clone(), name_expr.span),
|
ExprKind::Ident(ref s) => (s.clone(), name_expr.span),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
@@ -669,7 +672,7 @@ impl<'src> Parser<'src> {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_struct_field_list(&mut self) -> Vec<StructField> {
|
fn parse_struct_field_list(&mut self) -> Vec<StructField<Parsed>> {
|
||||||
let mut fields = Vec::new();
|
let mut fields = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
if matches!(self.current().kind, TokenKind::RCurly | TokenKind::Eof) {
|
if matches!(self.current().kind, TokenKind::RCurly | TokenKind::Eof) {
|
||||||
@@ -685,7 +688,7 @@ impl<'src> Parser<'src> {
|
|||||||
fields
|
fields
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_struct_field(&mut self) -> StructField {
|
fn parse_struct_field(&mut self) -> StructField<Parsed> {
|
||||||
let name_tok = self.expect(TokenKind::Ident);
|
let name_tok = self.expect(TokenKind::Ident);
|
||||||
self.expect(TokenKind::Colon);
|
self.expect(TokenKind::Colon);
|
||||||
// Struct literals allowed inside field values.
|
// Struct literals allowed inside field values.
|
||||||
@@ -702,7 +705,7 @@ impl<'src> Parser<'src> {
|
|||||||
// ── Top-level definitions ─────────────────────────────────────────────────
|
// ── Top-level definitions ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Parse an entire source file as a `Program`.
|
/// Parse an entire source file as a `Program`.
|
||||||
pub fn parse_program(&mut self) -> Program {
|
pub fn parse_program(&mut self) -> Program<Parsed> {
|
||||||
let start = self.current().span;
|
let start = self.current().span;
|
||||||
let mut defs = Vec::new();
|
let mut defs = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
@@ -716,7 +719,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse one top-level definition (`fn` or `struct`).
|
/// Parse one top-level definition (`fn` or `struct`).
|
||||||
pub fn parse_top_level_def(&mut self) -> TopLevelDef {
|
pub fn parse_top_level_def(&mut self) -> TopLevelDef<Parsed> {
|
||||||
let tok = self.current();
|
let tok = self.current();
|
||||||
match tok.kind {
|
match tok.kind {
|
||||||
TokenKind::Fn => self.parse_func_def(),
|
TokenKind::Fn => self.parse_func_def(),
|
||||||
@@ -749,7 +752,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_func_def(&mut self) -> TopLevelDef {
|
fn parse_func_def(&mut self) -> TopLevelDef<Parsed> {
|
||||||
let kw = self.advance(); // consume `fn`
|
let kw = self.advance(); // consume `fn`
|
||||||
let name_tok = self.expect(TokenKind::Ident);
|
let name_tok = self.expect(TokenKind::Ident);
|
||||||
self.expect(TokenKind::LParen);
|
self.expect(TokenKind::LParen);
|
||||||
@@ -809,7 +812,7 @@ impl<'src> Parser<'src> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_struct_def(&mut self) -> TopLevelDef {
|
fn parse_struct_def(&mut self) -> TopLevelDef<Parsed> {
|
||||||
let kw = self.advance(); // consume `struct`
|
let kw = self.advance(); // consume `struct`
|
||||||
let name_tok = self.expect(TokenKind::Ident);
|
let name_tok = self.expect(TokenKind::Ident);
|
||||||
self.expect(TokenKind::LCurly);
|
self.expect(TokenKind::LCurly);
|
||||||
@@ -855,7 +858,7 @@ impl<'src> Parser<'src> {
|
|||||||
|
|
||||||
/// Parse `arg, arg, …` up to `)`. The opening `(` has already been
|
/// Parse `arg, arg, …` up to `)`. The opening `(` has already been
|
||||||
/// consumed by `parse_led`. Returns `(args, close_span)`.
|
/// consumed by `parse_led`. Returns `(args, close_span)`.
|
||||||
fn parse_arg_list(&mut self) -> (Vec<Expr>, Span) {
|
fn parse_arg_list(&mut self) -> (Vec<Expr<Parsed>>, Span) {
|
||||||
let mut args = Vec::new();
|
let mut args = Vec::new();
|
||||||
loop {
|
loop {
|
||||||
if matches!(self.current().kind, TokenKind::RParen | TokenKind::Eof) {
|
if matches!(self.current().kind, TokenKind::RParen | TokenKind::Eof) {
|
||||||
@@ -879,21 +882,21 @@ impl<'src> Parser<'src> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::ast::{ElseBranch, ExprKind, StmtKind, TopLevelDefKind, Type};
|
use crate::ast::{ElseBranch, ExprKind, Parsed, StmtKind, TopLevelDefKind, Type};
|
||||||
|
|
||||||
// ── Expression test helpers ───────────────────────────────────────────────
|
// ── Expression test helpers ───────────────────────────────────────────────
|
||||||
|
|
||||||
fn parse(src: &str) -> Expr {
|
fn parse(src: &str) -> Expr<Parsed> {
|
||||||
Parser::new(src).parse_expr(true)
|
Parser::new(src).parse_expr(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_no_struct(src: &str) -> Expr {
|
fn parse_no_struct(src: &str) -> Expr<Parsed> {
|
||||||
Parser::new(src).parse_expr(false)
|
Parser::new(src).parse_expr(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Statement test helpers ────────────────────────────────────────────────
|
// ── Statement test helpers ────────────────────────────────────────────────
|
||||||
|
|
||||||
fn stmt(src: &str) -> Stmt {
|
fn stmt(src: &str) -> Stmt<Parsed> {
|
||||||
Parser::new(src).parse_stmt()
|
Parser::new(src).parse_stmt()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1223,7 +1226,10 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn type_nested_pointer() {
|
fn type_nested_pointer() {
|
||||||
// `**i32` → Pointer { Pointer { I32 } }
|
// `**i32` → Pointer { Pointer { I32 } }
|
||||||
assert!(matches!(parse_type_str("**i32"), Type::Pointer { mutable: false, .. }));
|
assert!(matches!(
|
||||||
|
parse_type_str("**i32"),
|
||||||
|
Type::Pointer { mutable: false, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1444,7 +1450,7 @@ mod tests {
|
|||||||
|
|
||||||
// ── Function definition tests ─────────────────────────────────────────────
|
// ── Function definition tests ─────────────────────────────────────────────
|
||||||
|
|
||||||
fn top(src: &str) -> TopLevelDef {
|
fn top(src: &str) -> TopLevelDef<Parsed> {
|
||||||
Parser::new(src).parse_top_level_def()
|
Parser::new(src).parse_top_level_def()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1503,7 +1509,10 @@ mod tests {
|
|||||||
let d = top("fn foo(p: *i32) { }");
|
let d = top("fn foo(p: *i32) { }");
|
||||||
match &d.kind {
|
match &d.kind {
|
||||||
TopLevelDefKind::Func(f) => {
|
TopLevelDefKind::Func(f) => {
|
||||||
assert!(matches!(f.params[0].ty, Type::Pointer { mutable: false, .. }));
|
assert!(matches!(
|
||||||
|
f.params[0].ty,
|
||||||
|
Type::Pointer { mutable: false, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_ => panic!("expected func def"),
|
_ => panic!("expected func def"),
|
||||||
}
|
}
|
||||||
@@ -1543,7 +1552,10 @@ mod tests {
|
|||||||
let d = top("struct Node { value: i32, next: *Node }");
|
let d = top("struct Node { value: i32, next: *Node }");
|
||||||
match &d.kind {
|
match &d.kind {
|
||||||
TopLevelDefKind::Struct(s) => {
|
TopLevelDefKind::Struct(s) => {
|
||||||
assert!(matches!(s.fields[1].ty, Type::Pointer { mutable: false, .. }));
|
assert!(matches!(
|
||||||
|
s.fields[1].ty,
|
||||||
|
Type::Pointer { mutable: false, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
_ => panic!("expected struct def"),
|
_ => panic!("expected struct def"),
|
||||||
}
|
}
|
||||||
@@ -1551,7 +1563,7 @@ mod tests {
|
|||||||
|
|
||||||
// ── Program tests ─────────────────────────────────────────────────────────
|
// ── Program tests ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
fn program(src: &str) -> Program {
|
fn program(src: &str) -> Program<Parsed> {
|
||||||
Parser::new(src).parse_program()
|
Parser::new(src).parse_program()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user