Files
PiCloud/dashboard/src/lib/rhai/ast.ts
MechaCat02 3d4c7b160b fix(dashboard): preserve blank lines and improve Rhai parser errors
Two follow-ups on the Rhai formatter shipped in 0.5.1.

* Formatter no longer collapses user-intent blank lines between
  statements. The lexer now records a side-channel list of offsets
  where the source contained two-or-more consecutive newlines; the
  formatter consults it and emits a single blank in the same spot
  (rustfmt's `blank_lines_upper_bound = 1` policy applied strictly —
  the prior forced blank between top-level `fn` decls is dropped, so
  the formatter never *adds* a blank the user didn't write).
* Parse errors now read like Rhai's own diagnostics. `expect()` takes
  an optional `role` hint and each call site supplies a domain phrase
  (`name of a variable`, `function name in function declaration`,
  `'{' to begin a block`, `name of a property`, …). End-of-input is
  reported as `script is incomplete`. The dashboard banner renders
  `Parse error: {message} (line L, position C)` with 1-based
  coordinates, matching Rhai's format exactly.

The FormatError payload also keeps the byte `offset` so callers that
want to drive the editor cursor (CodeMirror works in offsets) still
have it.

Also folds the workspace Cargo.lock version bumps for 0.5.1 — the
lock-file rewrite that should have travelled with the prior commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 21:26:42 +02:00

280 lines
5.8 KiB
TypeScript

// AST node definitions for the dashboard's hand-rolled Rhai parser.
//
// Every node carries `start` / `end` byte offsets into the source so the
// editor features (autocomplete, goto-def, find-usages, format) can map
// between positions in the document and nodes in the tree.
//
// The shape mirrors the Rhai book grammar (https://rhai.rs/book/language/)
// but simplified: type annotations are absent (Rhai is dynamic), and
// statement-vs-expression duality is collapsed by letting `if` / `switch` /
// block expressions appear in both positions (an `ExprStmt` wrapper turns
// any expression into a statement).
export interface Range {
start: number;
end: number;
}
// ---------------------------------------------------------------------------
// Comments — captured by the lexer with their positions and re-emitted by
// the formatter. Kept off the AST tree so they don't clutter walkers.
// ---------------------------------------------------------------------------
export interface Comment extends Range {
kind: 'LineComment' | 'BlockComment';
text: string;
}
// ---------------------------------------------------------------------------
// Statements
// ---------------------------------------------------------------------------
export type Stmt =
| LetStmt
| ConstStmt
| FnDecl
| ExprStmt
| ReturnStmt
| WhileStmt
| LoopStmt
| ForStmt
| BreakStmt
| ContinueStmt
| TryStmt;
export interface LetStmt extends Range {
kind: 'Let';
name: string;
nameRange: Range;
init: Expr | null;
}
export interface ConstStmt extends Range {
kind: 'Const';
name: string;
nameRange: Range;
init: Expr | null;
}
export interface Param extends Range {
name: string;
}
export interface FnDecl extends Range {
kind: 'FnDecl';
name: string;
nameRange: Range;
params: Param[];
body: BlockExpr;
}
export interface ExprStmt extends Range {
kind: 'ExprStmt';
expr: Expr;
// Whether the statement is terminated with `;`. Block-form expressions
// (`if`/`switch`/`{...}`) don't require it; everything else does.
semi: boolean;
}
export interface ReturnStmt extends Range {
kind: 'Return';
value: Expr | null;
}
export interface WhileStmt extends Range {
kind: 'While';
cond: Expr;
body: BlockExpr;
}
export interface LoopStmt extends Range {
kind: 'Loop';
body: BlockExpr;
}
export interface ForStmt extends Range {
kind: 'For';
varName: string;
varRange: Range;
iter: Expr;
body: BlockExpr;
}
export interface BreakStmt extends Range {
kind: 'Break';
}
export interface ContinueStmt extends Range {
kind: 'Continue';
}
export interface TryStmt extends Range {
kind: 'Try';
body: BlockExpr;
catchVar: string | null;
catchVarRange: Range | null;
handler: BlockExpr;
}
// ---------------------------------------------------------------------------
// Expressions
// ---------------------------------------------------------------------------
export type Expr =
| IdentExpr
| NumberExpr
| StringExpr
| BoolExpr
| NullExpr
| CallExpr
| MemberExpr
| IndexExpr
| UnaryExpr
| BinaryExpr
| AssignExpr
| ParenExpr
| ObjectMapExpr
| ArrayExpr
| FnExpr
| IfExpr
| SwitchExpr
| BlockExpr;
export interface IdentExpr extends Range {
kind: 'Ident';
name: string;
}
export interface NumberExpr extends Range {
kind: 'Number';
raw: string;
}
export interface StringExpr extends Range {
kind: 'String';
// The surrounding quote — `"` is escape-processed, backtick is raw and
// may span multiple lines. We don't decode escapes; the formatter just
// preserves the raw text between the quotes.
quote: '"' | '`';
raw: string;
}
export interface BoolExpr extends Range {
kind: 'Bool';
value: boolean;
}
export interface NullExpr extends Range {
kind: 'Null';
}
export interface CallExpr extends Range {
kind: 'Call';
callee: Expr;
args: Expr[];
}
export interface MemberExpr extends Range {
kind: 'Member';
object: Expr;
property: string;
propertyRange: Range;
}
export interface IndexExpr extends Range {
kind: 'Index';
object: Expr;
index: Expr;
}
export interface UnaryExpr extends Range {
kind: 'Unary';
op: string;
operand: Expr;
}
export interface BinaryExpr extends Range {
kind: 'Binary';
op: string;
left: Expr;
right: Expr;
}
export interface AssignExpr extends Range {
kind: 'Assign';
op: string; // = += -= *= /= %= ??=
target: Expr;
value: Expr;
}
export interface ParenExpr extends Range {
kind: 'Paren';
expr: Expr;
}
export interface ObjectMapEntry extends Range {
key: string;
keyRange: Range;
value: Expr;
}
export interface ObjectMapExpr extends Range {
kind: 'ObjectMap';
entries: ObjectMapEntry[];
}
export interface ArrayExpr extends Range {
kind: 'Array';
elements: Expr[];
}
export interface FnExpr extends Range {
kind: 'FnExpr';
params: Param[];
body: BlockExpr;
}
export interface IfExpr extends Range {
kind: 'IfExpr';
cond: Expr;
then: BlockExpr;
// else branch: either a block or another `if` for `else if` chains.
else_: BlockExpr | IfExpr | null;
}
export interface SwitchArm extends Range {
pattern: Expr | null; // null = `_` default case
guard: Expr | null;
value: Expr;
}
export interface SwitchExpr extends Range {
kind: 'SwitchExpr';
subject: Expr;
arms: SwitchArm[];
}
export interface BlockExpr extends Range {
kind: 'BlockExpr';
stmts: Stmt[];
}
// ---------------------------------------------------------------------------
// Top-level parse output
// ---------------------------------------------------------------------------
export interface ParseError extends Range {
message: string;
}
export interface ParseResult {
source: string;
program: BlockExpr;
errors: ParseError[];
comments: Comment[];
// Offsets at which the source contained a blank line (a whitespace
// run with two or more newlines). One entry per blank run; the
// formatter consults these to preserve user-intent vertical grouping.
blankLines: number[];
}