diff --git a/src/demo/DslExplorerPage.tsx b/src/demo/DslExplorerPage.tsx new file mode 100644 index 0000000..14dee4d --- /dev/null +++ b/src/demo/DslExplorerPage.tsx @@ -0,0 +1,82 @@ +import { useState } from "react"; +import { tokenizeExpression, parseExpression, validateAst, extractDependencies, FUNCTION_SIGNATURES, type Expr, type Token } from "../lib/questionnaire/dsl"; +import { QuestionnaireEngine } from "../lib/questionnaire/questionnaire-engine"; +import starter from "./editor-questionnaire.json"; +import { JsonViewer } from "./JsonViewer"; + +const examples = [ + { name: "Eligibility", expression: "age(answer('dateOfBirth'), today()) >= 18 && answer('consent') === true" }, + { name: "Precedence", expression: "1 + 2 * 3" }, + { name: "Branching", expression: "derived('inPerson') ? 'Welcome to the venue' : 'We will email your joining link'" }, + { name: "Collections", expression: "contains(answer('topics'), 'DESIGN') && count(answer('topics')) <= 2" }, + { name: "Validation value", expression: "length(trim(value)) >= 3" }, + { name: "Null fallback", expression: "answer('experience') ?? 0" }, + { name: "Short circuit", expression: "false && (1 / 0 > 0)" }, + { name: "Parser error", expression: "1 +" }, + { name: "Validation error", expression: "unknown(1)" }, + { name: "Evaluation error", expression: "1 / 0" }, +]; +const initialContext = JSON.stringify({ + answers: { name: "Alex", dateOfBirth: "1992-03-04", attendance: "IN_PERSON", guests: 1, needsSupport: false, topics: ["DESIGN"], experience: 2, consent: true }, + value: "Alex", today: "2026-09-28", +}, null, 2); +const engine = new QuestionnaireEngine(starter); +type Stage = { name: string; status: "Passed" | "Failed" | "Not run"; detail: string }; +function inspect(expression: string, context: string) { + const stages: Stage[] = [ + { name: "Tokenizer", status: "Not run", detail: "Split source into tokens." }, + { name: "Parser", status: "Not run", detail: "Build an AST using operator precedence." }, + { name: "Validation", status: "Not run", detail: "Check allowed identifiers, function names, arity, and literal IDs." }, + { name: "Evaluation", status: "Not run", detail: "Run with the questionnaire session and input context." }, + ]; + let tokens: Token[] = [], ast: Expr | undefined, result: unknown; + let index = 0; + try { + tokens = tokenizeExpression(expression); stages[0] = { ...stages[0], status: "Passed", detail: `${tokens.length} tokens, including end of input.` }; index++; + ast = parseExpression(expression); stages[1] = { ...stages[1], status: "Passed", detail: `Root node: ${ast.type}.` }; index++; + validateAst(ast); stages[2] = { ...stages[2], status: "Passed", detail: "Expression passes DSL validation." }; index++; + const input = JSON.parse(context); + if (!input || typeof input !== "object" || Array.isArray(input) || !input.answers || typeof input.answers !== "object" || Array.isArray(input.answers) || typeof input.today !== "string") throw new Error("Context needs an answers object and a today date string."); + result = engine.createSession(input.answers, { today: input.today }).evaluate(expression, input.value); + stages[3] = { ...stages[3], status: "Passed", detail: `Result type: ${result === null ? "null" : Array.isArray(result) ? "array" : typeof result}.` }; + } catch (cause) { stages[index] = { ...stages[index], status: "Failed", detail: cause instanceof Error ? cause.message : String(cause) }; } + return { expression, context, stages, tokens, ast, result }; +} + +function AstTree({ node, edge = "root" }: { node: Expr; edge?: string }) { + const title = node.type === "literal" ? `literal ${JSON.stringify(node.value)}` : node.type === "call" ? `call ${node.name}()` : node.type === "identifier" ? `identifier ${node.name}` : node.type === "binary" || node.type === "unary" ? `${node.type} ${node.operator}` : node.type; + const children: [string, Expr][] = node.type === "binary" ? [["left", node.left], ["right", node.right]] : node.type === "unary" ? [["argument", node.argument]] : node.type === "conditional" ? [["condition", node.condition], ["true", node.whenTrue], ["false", node.whenFalse]] : node.type === "call" ? node.args.map((arg, i) => [`arg ${i + 1}`, arg]) : node.type === "array" ? node.items.map((item, i) => [`item ${i}`, item]) : []; + return
  • {children.length ?
    {edge}{title}
    :
    {edge}{title}
    }
  • ; +} + +export function DslExplorerPage() { + const [expression, setExpression] = useState(examples[0].expression); + const [context, setContext] = useState(initialContext); + const [analysis, setAnalysis] = useState(() => inspect(examples[0].expression, initialContext)); + const [tab, setTab] = useState("AST tree"); + const stale = expression !== analysis.expression || context !== analysis.context; + const tabs = ["AST tree", "AST JSON", "Dependencies", "Functions"]; + return
    +

    Inside the expression engine

    DSL playground

    Follow an expression from source text to tokens, syntax tree, and result.

    +
    +

    Expression

    +