brink-syntax 0.0.16

Syntax types and parser for inkle's ink narrative scripting language
Documentation
mod cst;

use super::check;
use crate::parse;

#[test]
fn return_bare() {
    check("~ return\n");
}

#[test]
fn return_with_expr() {
    check("~ return 5\n");
}

#[test]
fn temp_declaration() {
    check("~ temp x = 5\n");
}

#[test]
fn assignment() {
    check("~ x = 10\n");
}

#[test]
fn compound_assign_plus() {
    check("~ x += 1\n");
}

#[test]
fn compound_assign_minus() {
    check("~ x -= 1\n");
}

#[test]
fn bare_expression() {
    check("~ foo()\n");
}

#[test]
fn bare_increment() {
    check("~ x++\n");
}

// ── T1b superset: multi-line `~ { … }` blocks (docs/t1b-surface-spec.md §2) ──

#[test]
fn block_empty() {
    check("~ {\n}\n");
}

#[test]
fn block_temp_and_assignment() {
    check("~ {\ntemp total = 0\ntotal = total + 1\n}\n");
}

#[test]
fn block_if_else() {
    check("~ {\nif total > 3 {\nscore = total\n} else {\nscore = 0\n}\n}\n");
}

#[test]
fn block_if_elseif_else() {
    check("~ {\nif a {\nx = 1\n} else if b {\nx = 2\n} else {\nx = 3\n}\n}\n");
}

#[test]
fn block_while() {
    check("~ {\nwhile total > 3 {\ntotal = total - 1\n}\n}\n");
}

#[test]
fn block_for_in() {
    check("~ {\nfor item in list {\ntotal = total + item\n}\n}\n");
}

#[test]
fn block_break_continue() {
    check("~ {\nwhile true {\nbreak\ncontinue\n}\n}\n");
}

// ── FlowFrame `await` (docs/flow-suspension-spec.md §3) ─────────────

/// `~ await cond` — top-level logic-line suspension point.
#[test]
fn await_logic_line() {
    check("~ await gold > 100\n");
}

/// `~ await some_fn_value` — a bare dynamic-condition suspension point.
#[test]
fn await_logic_line_bare_ident() {
    check("~ await alarm_raised\n");
}

/// `await cond` inside a `~ { … }` block.
#[test]
fn block_await() {
    check("~ {\nawait gold > 100\n}\n");
}

/// `while await cond { … }` — the persistent-await loop.
#[test]
fn block_while_await() {
    check("~ {\nwhile await alarm_raised {\ntotal = total - 1\n}\n}\n");
}

/// `await` stays an ordinary assignable identifier — `await = 5` is an
/// assignment to a variable named `await`, not a suspension point.
#[test]
fn await_as_assignment_target() {
    check("~ {\nawait = 5\n}\n");
}

/// `while await { … }` (a plain loop over a variable literally named
/// `await`) is NOT the persistent-await form — `{` follows `await`
/// directly, so `await` is the loop condition.
#[test]
fn while_over_variable_named_await() {
    check("~ {\nwhile await {\ntotal = total - 1\n}\n}\n");
}

#[test]
fn block_return_bare() {
    check("~ {\nreturn\n}\n");
}

#[test]
fn block_return_with_value() {
    check("~ {\nreturn 5\n}\n");
}

#[test]
fn block_expr_stmt() {
    check("~ {\nfoo()\n}\n");
}

#[test]
fn block_indexed_assignment() {
    check("~ {\ngrid[y][x] = v\n}\n");
}

/// `arr[i].field = v` — a mixed index-then-field lvalue (issue #674). The
/// grammar must recognize this as an `ASSIGNMENT`, not fall through to a
/// bare expression followed by a dangling `= v`. LIR still rejects it as a
/// chained/mixed field write (`E074`) — this test only pins the grammar.
#[test]
fn block_index_then_field_assignment() {
    check("~ {\narr[i].x = 2.0\n}\n");
}

/// Same shape, compound assignment operator.
#[test]
fn block_index_then_field_compound_assignment() {
    check("~ {\narr[i].x += 1.0\n}\n");
}

/// The classic top-level `~` logic line (not a `~ { … }` block) must accept
/// the same lvalue shape.
#[test]
fn top_level_index_then_field_assignment() {
    check("~ arr[i].x = 2.0\n");
}

/// Chained further: `arr[i].x[j] = v` — index, field, index again.
#[test]
fn block_index_field_index_assignment() {
    check("~ {\narr[i].x[j] = v\n}\n");
}

#[test]
fn block_array_literal_for_loop() {
    check("~ {\nfor item in #[1, 2, 3] {\ntotal = total + item\n}\n}\n");
}

/// `if`/`while`/`for`/`break`/`continue`/`in` are contextual keywords — they
/// must stay ordinary identifiers everywhere outside a `~ { … }` block.
#[test]
fn block_keywords_are_contextual_not_reserved() {
    check("~ if = 5\n");
    check("~ for = 1\n");
    check("~ while = 2\n");
    check("~ break = 3\n");
    check("~ continue = 4\n");
    check("~ in = 6\n");
}

#[test]
fn insta_block() {
    let p = parse("~ {\ntemp x = 0\nif x > 0 {\nx = x - 1\n}\n}\n");
    insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}

/// `arr[i].x = 2.0` (issue #674) — pins the CST shape: an `ASSIGNMENT`
/// target of `FIELD_ACCESS_EXPR { INDEX_EXPR { … }, IDENTIFIER }`, mirroring
/// the equivalent expression-position shape (see
/// `insta_field_access_after_struct_literal` in the expression test suite).
#[test]
fn insta_index_then_field_assignment() {
    let p = parse("~ arr[i].x = 2.0\n");
    assert!(p.errors().is_empty(), "errors: {:?}", p.errors());
    insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}

#[test]
fn insta_temp_decl() {
    let p = parse("~ temp x = 5\n");
    insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}

#[test]
fn insta_return_with_expr() {
    let p = parse("~ return x + 1\n");
    insta::assert_snapshot!(format!("{:#?}", p.syntax()));
}