From e810855933199c9040898e54ae312f6861a28ef1 Mon Sep 17 00:00:00 2001 From: Nilay Majorwar Date: Fri, 18 Feb 2022 16:57:59 +0530 Subject: [PATCH] Add Shakespeare esolang --- languages/shakespeare/README.md | 13 + languages/shakespeare/common.ts | 195 +++++ languages/shakespeare/engine.ts | 4 + languages/shakespeare/index.ts | 11 + languages/shakespeare/input-stream.ts | 46 ++ languages/shakespeare/parser/constants.ts | 74 ++ languages/shakespeare/parser/cst.d.ts | 671 ++++++++++++++++++ .../shakespeare/parser/generate-cst-types.ts | 18 + languages/shakespeare/parser/index.ts | 54 ++ languages/shakespeare/parser/parser.ts | 589 +++++++++++++++ languages/shakespeare/parser/tokens.ts | 509 +++++++++++++ .../shakespeare/parser/visitor-types.d.ts | 323 +++++++++ languages/shakespeare/parser/visitor.ts | 617 ++++++++++++++++ .../shakespeare/renderer/character-row.tsx | 24 + languages/shakespeare/renderer/index.tsx | 63 ++ languages/shakespeare/renderer/topbar.tsx | 56 ++ languages/shakespeare/renderer/utils.tsx | 60 ++ languages/shakespeare/runtime.ts | 355 +++++++++ languages/shakespeare/tests/helloworld.txt | 89 +++ languages/shakespeare/tests/index.test.ts | 24 + languages/shakespeare/tests/primes.txt | 104 +++ languages/shakespeare/tests/reverse.txt | 45 ++ package.json | 2 + pages/ide/shakespeare.tsx | 24 + pages/languages.json | 6 +- yarn.lock | 132 +++- 26 files changed, 4106 insertions(+), 2 deletions(-) create mode 100644 languages/shakespeare/README.md create mode 100644 languages/shakespeare/common.ts create mode 100644 languages/shakespeare/engine.ts create mode 100644 languages/shakespeare/index.ts create mode 100644 languages/shakespeare/input-stream.ts create mode 100644 languages/shakespeare/parser/constants.ts create mode 100644 languages/shakespeare/parser/cst.d.ts create mode 100644 languages/shakespeare/parser/generate-cst-types.ts create mode 100644 languages/shakespeare/parser/index.ts create mode 100644 languages/shakespeare/parser/parser.ts create mode 100644 languages/shakespeare/parser/tokens.ts create mode 100644 languages/shakespeare/parser/visitor-types.d.ts create mode 100644 languages/shakespeare/parser/visitor.ts create mode 100644 languages/shakespeare/renderer/character-row.tsx create mode 100644 languages/shakespeare/renderer/index.tsx create mode 100644 languages/shakespeare/renderer/topbar.tsx create mode 100644 languages/shakespeare/renderer/utils.tsx create mode 100644 languages/shakespeare/runtime.ts create mode 100644 languages/shakespeare/tests/helloworld.txt create mode 100644 languages/shakespeare/tests/index.test.ts create mode 100644 languages/shakespeare/tests/primes.txt create mode 100644 languages/shakespeare/tests/reverse.txt create mode 100644 pages/ide/shakespeare.tsx diff --git a/languages/shakespeare/README.md b/languages/shakespeare/README.md new file mode 100644 index 0000000..b644adb --- /dev/null +++ b/languages/shakespeare/README.md @@ -0,0 +1,13 @@ +# Shakespeare + +## References + +- Official documentation and implementation: http://shakespearelang.sourceforge.net + +## Implementation details + +- It is not necessary for questions to immediately be followed by conditionals. Conditionals + must immediately follow a question though - a runtime error is thrown otherwise. + +- Empty acts and scenes are invalid. An act must contain at least one scene, and a scene must contain + at least one dialogue set or entry-exit clause. diff --git a/languages/shakespeare/common.ts b/languages/shakespeare/common.ts new file mode 100644 index 0000000..64aa96c --- /dev/null +++ b/languages/shakespeare/common.ts @@ -0,0 +1,195 @@ +import { MonacoTokensProvider } from "../types"; +import * as R from "./parser/constants"; + +/** Runtime value of a character */ +export type CharacterValue = { + value: number; + stack: number[]; +}; + +/** Bag of characters declared in the program */ +export type CharacterBag = { [name: string]: CharacterValue }; + +/** Type of props passed to renderer */ +export type RS = { + currentSpeaker: string | null; + charactersOnStage: string[]; + characterBag: CharacterBag; + questionState: boolean | null; +}; + +/** Sample program */ +export const sampleProgram = `The Infamous Hello World Program. + +Romeo, a young man with a remarkable patience. +Juliet, a likewise young woman of remarkable grace. +Ophelia, a remarkable woman much in dispute with Hamlet. +Hamlet, the flatterer of Andersen Insulting A/S. + + + Act I: Hamlet's insults and flattery. + + Scene I: The insulting of Romeo. + +[Enter Hamlet and Romeo] + +Hamlet: + You lying stupid fatherless big smelly half-witted coward! + You are as stupid as the difference between a handsome rich brave + hero and thyself! Speak your mind! + + You are as brave as the sum of your fat little stuffed misused dusty + old rotten codpiece and a beautiful fair warm peaceful sunny summer's day. + You are as healthy as the difference between the sum of the sweetest + reddest rose and my father and yourself! Speak your mind! + + You are as cowardly as the sum of yourself and the difference + between a big mighty proud kingdom and a horse. Speak your mind. + + Speak your mind! + +[Exit Romeo] + + Scene II: The praising of Juliet. + +[Enter Juliet] + +Hamlet: + Thou art as sweet as the sum of the sum of Romeo and his horse and his + black cat! Speak thy mind! + +[Exit Juliet] + + Scene III: The praising of Ophelia. + +[Enter Ophelia] + +Hamlet: + Thou art as lovely as the product of a large rural town and my amazing + bottomless embroidered purse. Speak thy mind! + + Thou art as loving as the product of the bluest clearest sweetest sky + and the sum of a squirrel and a white horse. Thou art as beautiful as + the difference between Juliet and thyself. Speak thy mind! + +[Exeunt Ophelia and Hamlet] + + + Act II: Behind Hamlet's back. + + Scene I: Romeo and Juliet's conversation. + +[Enter Romeo and Juliet] + +Romeo: + Speak your mind. You are as worried as the sum of yourself and the + difference between my small smooth hamster and my nose. Speak your mind! + +Juliet: + Speak YOUR mind! You are as bad as Hamlet! You are as small as the + difference between the square of the difference between my little pony + and your big hairy hound and the cube of your sorry little + codpiece. Speak your mind! + +[Exit Romeo] + + Scene II: Juliet and Ophelia's conversation. + +[Enter Ophelia] + +Juliet: + Thou art as good as the quotient between Romeo and the sum of a small + furry animal and a leech. Speak your mind! + +Ophelia: + Thou art as disgusting as the quotient between Romeo and twice the + difference between a mistletoe and an oozing infected blister! + Speak your mind! + +[Exeunt] + +`; + +/** Syntax highlighting */ +export const editorTokensProvider: MonacoTokensProvider = { + ignoreCase: true, + tokenizer: { + /** Program title */ + root: [[/[^\.]+\./, { token: "meta", next: "introduction" }]], + /** Character introductions */ + introduction: [ + { include: "whitespace" }, + [ + `${R.CharacterRegex.source}(,\\s*)([^\\.]+\\.\\s*\\n?)`, + ["tag", "", "comment"], + ], + [ + /(act)(\s+)([IVXLCDM]+)(:\s+)([^\.]+\.)/, + // prettier-ignore + ["keyword", "", "constant", "", { token: "comment", next: "actSection" }] as any, + ], + ], + /** A single act of the play */ + actSection: [ + { include: "whitespace" }, + [/(?=act\b)/, { token: "", next: "@pop" }], + [ + /(scene)(\s+)([IVXLCDM]+)(:\s+)(.+?(?:\.|\!|\?))/, + // prettier-ignore + ["constant.function", "", "constant", "", {token: "comment", next: "sceneSection"}] as any, + ], + ], + /** A single scene of an act in the play */ + sceneSection: [ + { include: "whitespace" }, + [/\[/, { token: "", next: "entryExitClause" }], + [/(?=act\b)/, { token: "", next: "@pop" }], + [/(?=scene\b)/, { token: "", next: "@pop" }], + { include: "dialogue" }, + ], + /** Dialogues spoken by characters */ + dialogue: [ + { include: "whitespace" }, + [`${R.CharacterRegex.source}:`, "tag"], + [R.CharacterRegex, "tag"], + [/nothing|zero/, "constant"], + [/(remember|recall)\b/, "keyword"], + [/(myself|i|me|thyself|yourself|thee|thou|you)\b/, "variable"], + [/speak (thine|thy|your) mind\b/, "constant.function"], + [/open (thine|thy|your) heart\b/, "constant.function"], + [/listen to (thine|thy|your) heart\b/, "constant.function"], + [/open (thine|thy|your) mind\b/, "constant.function"], + [ + /(punier|smaller|worse|better|bigger|fresher|friendlier|nicer|jollier)\b/, + "attribute", + ], + [/(more|less)\b/, "attribute"], + [/if (so|not),/, "keyword"], + [ + /((?:let us|we shall|we must) (?:return|proceed) to (?:act|scene) )([IVXLCDM]+)/, + ["annotation", "constant"], + ], + [ + /(sum|difference|product|quotient|remainder|factorial|square|root|cube|twice)\b/, + "operators", + ], + [R.PositiveNounRegex, "constant"], + [R.NeutralNounRegex, "constant"], + [R.NegativeNounRegex, "constant"], + [R.PositiveAdjectiveRegex, "attribute"], + [R.NeutralAdjectiveRegex, "attribute"], + [R.NegativeAdjectiveRegex, "attribute"], + ], + /** Clause for entry/exit of character(s) */ + entryExitClause: [ + { include: "whitespace" }, + [/enter|exit|exeunt/, "keyword"], + [/]/, { token: "", next: "@pop" }], + [R.CharacterRegex, "tag"], + [/and\b|,/, ""], + ], + /** Utility: skip across whitespace and line breaks */ + whitespace: [[/[\s\n]+/, ""]], + }, + defaultToken: "", +}; diff --git a/languages/shakespeare/engine.ts b/languages/shakespeare/engine.ts new file mode 100644 index 0000000..98d8710 --- /dev/null +++ b/languages/shakespeare/engine.ts @@ -0,0 +1,4 @@ +import { setupWorker } from "../setup-worker"; +import XYZLanguageEngine from "./runtime"; + +setupWorker(new XYZLanguageEngine()); diff --git a/languages/shakespeare/index.ts b/languages/shakespeare/index.ts new file mode 100644 index 0000000..c9a1de8 --- /dev/null +++ b/languages/shakespeare/index.ts @@ -0,0 +1,11 @@ +import { Renderer } from "./renderer"; +import { LanguageProvider } from "../types"; +import { RS, sampleProgram, editorTokensProvider } from "./common"; + +const provider: LanguageProvider = { + Renderer, + sampleProgram, + editorTokensProvider, +}; + +export default provider; diff --git a/languages/shakespeare/input-stream.ts b/languages/shakespeare/input-stream.ts new file mode 100644 index 0000000..e22b16b --- /dev/null +++ b/languages/shakespeare/input-stream.ts @@ -0,0 +1,46 @@ +import { RuntimeError } from "../worker-errors"; + +/** + * A barebones input stream implementation for consuming integers and characters from a string. + */ +export default class InputStream { + private _text: string; + + /** Create a new input stream loaded with the given input */ + constructor(text: string) { + this._text = text; + } + + /** Remove leading whitespace from the current input stream */ + private exhaustLeadingWhitespace(): void { + const firstChar = this._text.trim()[0]; + const posn = this._text.search(firstChar); + this._text = this._text.slice(posn); + } + + /** Parse input stream for an integer */ + getNumber(): number { + this.exhaustLeadingWhitespace(); + // The extra whitespace differentiates whether string is empty or all numbers. + if (this._text === "") throw new RuntimeError("Unexpected end of input"); + let posn = this._text.search(/[^0-9]/); + if (posn === 0) + throw new RuntimeError(`Unexpected input character: '${this._text[0]}'`); + if (posn === -1) posn = this._text.length; + // Consume and parse numeric part + const numStr = this._text.slice(0, posn); + this._text = this._text.slice(posn); + return parseInt(numStr, 10); + } + + /** + * Parse input stream for the first character, and return its ASCII code. + * If end of input, returns -1. + */ + getChar(): number { + if (this._text.length === 0) return -1; + const char = this._text[0]; + this._text = this._text.slice(1); + return char.charCodeAt(0); + } +} diff --git a/languages/shakespeare/parser/constants.ts b/languages/shakespeare/parser/constants.ts new file mode 100644 index 0000000..ace7856 --- /dev/null +++ b/languages/shakespeare/parser/constants.ts @@ -0,0 +1,74 @@ +/** Regex bit to accommodate line breaks in multi-word keywords */ +const br = /(?: |\s*\n\s*)/.source; + +/** Utility to create regex from list of words */ +const makeRegex = (words: string[], flags?: string) => { + const fixedWords = words.map((w) => w.replace(/ /g, br)); + const pattern = "(" + fixedWords.join("|") + ")\\b"; + return new RegExp(pattern, flags); +}; + +// prettier-ignore +const CHARACTERS = ["Achilles", "Adonis", "Adriana", "Aegeon", "Aemilia", "Agamemnon", "Agrippa", "Ajax", "Alonso", "Andromache", +"Angelo", "Antiochus", "Antonio", "Arthur", "Autolycus", "Balthazar", "Banquo", "Beatrice", "Benedick", "Benvolio", "Bianca", +"Brabantio", "Brutus", "Capulet", "Cassandra", "Cassius", "Christopher Sly", "Cicero", "Claudio", "Claudius", "Cleopatra", +"Cordelia", "Cornelius", "Cressida", "Cymberline", "Demetrius", "Desdemona", "Dionyza", "Doctor Caius", "Dogberry", "Don John", +"Don Pedro", "Donalbain", "Dorcas", "Duncan", "Egeus", "Emilia", "Escalus", "Falstaff", "Fenton", "Ferdinand", "Ford", "Fortinbras", +"Francisca", "Friar John", "Friar Laurence", "Gertrude", "Goneril", "Hamlet", "Hecate", "Hector", "Helen", "Helena", "Hermia", +"Hermonie", "Hippolyta", "Horatio", "Imogen", "Isabella", "John of Gaunt", "John of Lancaster", "Julia", "Juliet", "Julius Caesar", +"King Henry", "King John", "King Lear", "King Richard", "Lady Capulet", "Lady Macbeth", "Lady Macduff", "Lady Montague", "Lennox", +"Leonato", "Luciana", "Lucio", "Lychorida", "Lysander", "Macbeth", "Macduff", "Malcolm", "Mariana", "Mark Antony", "Mercutio", +"Miranda", "Mistress Ford", "Mistress Overdone", "Mistress Page", "Montague", "Mopsa", "Oberon", "Octavia", "Octavius Caesar", +"Olivia", "Ophelia", "Orlando", "Orsino", "Othello", "Page", "Pantino", "Paris", "Pericles", "Pinch", "Polonius", "Pompeius", +"Portia", "Priam", "Prince Henry", "Prospero", "Proteus", "Publius", "Puck", "Queen Elinor", "Regan", "Robin", "Romeo", "Rosalind", +"Sebastian", "Shallow", "Shylock", "Slender", "Solinus", "Stephano", "Thaisa", "The Abbot of Westminster", "The Apothecary", +"The Archbishop of Canterbury", "The Duke of Milan", "The Duke of Venice", "The Ghost", "Theseus", "Thurio", "Timon", "Titania", +"Titus", "Troilus", "Tybalt", "Ulysses", "Valentine", "Venus", "Vincentio", "Viola"] + +/** Regex that matches an identified character name */ +export const CharacterRegex = makeRegex(CHARACTERS, "i"); + +// prettier-ignore +const NEGATIVE_ADJECTIVE = ["bad","cowardly","cursed","damned","dirty","disgusting", "distasteful","dusty","evil","fat-kidneyed", +"fat","fatherless","foul","hairy","half-witted", "horrible","horrid","infected","lying","miserable","misused", +"oozing","rotten","rotten","smelly","snotty","sorry", "stinking","stuffed","stupid","vile","villainous","worried"] + +/** Regex that matches a negative adjective */ +export const NegativeAdjectiveRegex = makeRegex(NEGATIVE_ADJECTIVE, "i"); + +// prettier-ignore +const NEGATIVE_NOUNS = ["Hell","Microsoft","bastard","beggar","blister","codpiece","coward","curse","death","devil","draught", +"famine","flirt-gill","goat","hate","hog","hound","leech","lie","pig","plague","starvation","toad","war","wolf"]; + +/** Regex that matches a negative noun */ +export const NegativeNounRegex = makeRegex(NEGATIVE_NOUNS, "i"); + +// prettier-ignore +const NEUTRAL_ADJECTIVES = ["big","black","blue","bluest","bottomless","furry","green","hard","huge","large","little", +"normal","old","purple","red","rural","small","tiny","white","yellow"]; + +/** Regex that matches a neutral adjective */ +export const NeutralAdjectiveRegex = makeRegex(NEUTRAL_ADJECTIVES, "i"); + +// prettier-ignore +const NEUTRAL_NOUNS = ["animal","aunt","brother","cat","chihuahua","cousin","cow","daughter","door","face","father", +"fellow","granddaughter","grandfather","grandmother","grandson","hair","hamster","horse","lamp","lantern","mistletoe", +"moon","morning","mother","nephew","niece","nose","purse","road","roman","sister","sky","son","squirrel","stone wall", +"thing","town","tree","uncle","wind"] + +/** Regex that matches a neutral noun */ +export const NeutralNounRegex = makeRegex(NEUTRAL_NOUNS, "i"); + +// prettier-ignore +const POSITIVE_ADJECTIVES = ["amazing","beautiful","blossoming","bold","brave","charming","clearest","cunning","cute", +"delicious","embroidered","fair","fine","gentle","golden","good","handsome","happy","healthy","honest","lovely","loving", +"mighty","noble","peaceful","pretty","prompt","proud","reddest","rich","smooth","sunny","sweet","sweetest","trustworthy","warm"] + +/** Regex that matches a positive adjective */ +export const PositiveAdjectiveRegex = makeRegex(POSITIVE_ADJECTIVES, "i"); + +// prettier-ignore +const POSITIVE_NOUN = ["Heaven","King","Lord","angel","flower","happiness","joy","plum","summer's day","hero","rose","kingdom","pony"] + +/** Regex that matches a positive noun */ +export const PositiveNounRegex = makeRegex(POSITIVE_NOUN, "i"); diff --git a/languages/shakespeare/parser/cst.d.ts b/languages/shakespeare/parser/cst.d.ts new file mode 100644 index 0000000..124e436 --- /dev/null +++ b/languages/shakespeare/parser/cst.d.ts @@ -0,0 +1,671 @@ +import type { CstNode, ICstVisitor, IToken } from "chevrotain"; + +export interface ProgramCstNode extends CstNode { + name: "program"; + children: ProgramCstChildren; +} + +export type ProgramCstChildren = { + programTitle: ProgramTitleCstNode[]; + WhitespaceOrNewline?: IToken[]; + characterIntro?: CharacterIntroCstNode[]; + actSection?: ActSectionCstNode[]; +}; + +export interface ProgramTitleCstNode extends CstNode { + name: "programTitle"; + children: ProgramTitleCstChildren; +} + +export type ProgramTitleCstChildren = { + Word?: IToken[]; + Period: IToken[]; +}; + +export interface CharacterIntroCstNode extends CstNode { + name: "characterIntro"; + children: CharacterIntroCstChildren; +} + +export type CharacterIntroCstChildren = { + Character: IToken[]; + Comma: IToken[]; + Word?: IToken[]; + SentenceMark: IToken[]; +}; + +export interface ActHeadingCstNode extends CstNode { + name: "actHeading"; + children: ActHeadingCstChildren; +} + +export type ActHeadingCstChildren = { + Act: IToken[]; + RomanNumeral: IToken[]; + Colon: IToken[]; + Word?: IToken[]; + SentenceMark: IToken[]; +}; + +export interface SceneHeadingCstNode extends CstNode { + name: "sceneHeading"; + children: SceneHeadingCstChildren; +} + +export type SceneHeadingCstChildren = { + Scene: IToken[]; + RomanNumeral: IToken[]; + Colon: IToken[]; + Word?: IToken[]; + SentenceMark: IToken[]; +}; + +export interface EntranceCstNode extends CstNode { + name: "entrance"; + children: EntranceCstChildren; +} + +export type EntranceCstChildren = { + SquareBracketOpen: IToken[]; + Enter: IToken[]; + Character: IToken[]; + And?: IToken[]; + SquareBracketClose: IToken[]; +}; + +export interface ExitCstNode extends CstNode { + name: "exit"; + children: ExitCstChildren; +} + +export type ExitCstChildren = { + SquareBracketOpen: IToken[]; + Exit: IToken[]; + Character: IToken[]; + SquareBracketClose: IToken[]; +}; + +export interface MultiExitCstNode extends CstNode { + name: "multiExit"; + children: MultiExitCstChildren; +} + +export type MultiExitCstChildren = { + SquareBracketOpen: IToken[]; + Exeunt: IToken[]; + Character?: IToken[]; + And?: IToken[]; + SquareBracketClose: IToken[]; +}; + +export interface EntryExitClauseCstNode extends CstNode { + name: "entryExitClause"; + children: EntryExitClauseCstChildren; +} + +export type EntryExitClauseCstChildren = { + entrance?: EntranceCstNode[]; + exit?: ExitCstNode[]; + multiExit?: MultiExitCstNode[]; +}; + +export interface ActSectionCstNode extends CstNode { + name: "actSection"; + children: ActSectionCstChildren; +} + +export type ActSectionCstChildren = { + actHeading: ActHeadingCstNode[]; + sceneSection: SceneSectionCstNode[]; +}; + +export interface SceneSectionCstNode extends CstNode { + name: "sceneSection"; + children: SceneSectionCstChildren; +} + +export type SceneSectionCstChildren = { + sceneHeading: SceneHeadingCstNode[]; + sceneSectionChunk: SceneSectionChunkCstNode[]; +}; + +export interface SceneSectionChunkCstNode extends CstNode { + name: "sceneSectionChunk"; + children: SceneSectionChunkCstChildren; +} + +export type SceneSectionChunkCstChildren = { + entryExitClause?: EntryExitClauseCstNode[]; + dialogueSet?: DialogueSetCstNode[]; +}; + +export interface SpeakerClauseCstNode extends CstNode { + name: "speakerClause"; + children: SpeakerClauseCstChildren; +} + +export type SpeakerClauseCstChildren = { + Character: IToken[]; + Colon: IToken[]; +}; + +export interface DialogueSetCstNode extends CstNode { + name: "dialogueSet"; + children: DialogueSetCstChildren; +} + +export type DialogueSetCstChildren = { + speakerClause: SpeakerClauseCstNode[]; + dialogueLine: DialogueLineCstNode[]; +}; + +export interface DialogueLineCstNode extends CstNode { + name: "dialogueLine"; + children: DialogueLineCstChildren; +} + +export type DialogueLineCstChildren = { + conditional?: ConditionalCstNode[]; + nonConditionalDialogueLine?: NonConditionalDialogueLineCstNode[]; +}; + +export interface NonConditionalDialogueLineCstNode extends CstNode { + name: "nonConditionalDialogueLine"; + children: NonConditionalDialogueLineCstChildren; +} + +export type NonConditionalDialogueLineCstChildren = { + assignment?: AssignmentCstNode[]; + stdin?: StdinCstNode[]; + stdout?: StdoutCstNode[]; + goto?: GotoCstNode[]; + stackPush?: StackPushCstNode[]; + stackPop?: StackPopCstNode[]; + question?: QuestionCstNode[]; +}; + +export interface NounCstNode extends CstNode { + name: "noun"; + children: NounCstChildren; +} + +export type NounCstChildren = { + NegativeNoun?: IToken[]; + NeutralNoun?: IToken[]; + PositiveNoun?: IToken[]; +}; + +export interface AdjectiveCstNode extends CstNode { + name: "adjective"; + children: AdjectiveCstChildren; +} + +export type AdjectiveCstChildren = { + NegativeAdjective?: IToken[]; + NeutralAdjective?: IToken[]; + PositiveAdjective?: IToken[]; +}; + +export interface PossessiveCstNode extends CstNode { + name: "possessive"; + children: PossessiveCstChildren; +} + +export type PossessiveCstChildren = { + FirstPersonPossessive?: IToken[]; + SecondPersonPossessive?: IToken[]; + ThirdPersonPossessive?: IToken[]; +}; + +export interface ReflexiveCstNode extends CstNode { + name: "reflexive"; + children: ReflexiveCstChildren; +} + +export type ReflexiveCstChildren = { + FirstPersonReflexive?: IToken[]; + SecondPersonReflexive?: IToken[]; +}; + +export interface ComparativeCstNode extends CstNode { + name: "comparative"; + children: ComparativeCstChildren; +} + +export type ComparativeCstChildren = { + PositiveComparative?: IToken[]; + NegativeComparative?: IToken[]; +}; + +export interface AssignmentCstNode extends CstNode { + name: "assignment"; + children: AssignmentCstChildren; +} + +export type AssignmentCstChildren = { + exclaimAssignment?: ExclaimAssignmentCstNode[]; + arithAssignment?: ArithAssignmentCstNode[]; +}; + +export interface ExclaimAssignmentCstNode extends CstNode { + name: "exclaimAssignment"; + children: ExclaimAssignmentCstChildren; +} + +export type ExclaimAssignmentCstChildren = { + SecondPerson: IToken[]; + unarticulatedVerbalConstant: UnarticulatedVerbalConstantCstNode[]; + SentenceMark: IToken[]; +}; + +export interface ArithAssignmentCstNode extends CstNode { + name: "arithAssignment"; + children: ArithAssignmentCstChildren; +} + +export type ArithAssignmentCstChildren = { + SecondPerson: IToken[]; + Be: IToken[]; + As?: IToken[]; + adjective?: AdjectiveCstNode[]; + expression: ExpressionCstNode[]; + SentenceMark: IToken[]; +}; + +export interface StdinCstNode extends CstNode { + name: "stdin"; + children: StdinCstChildren; +} + +export type StdinCstChildren = { + Listen?: IToken[]; + To?: IToken[]; + SecondPersonPossessive?: IToken[]; + Heart?: IToken[]; + Open?: IToken[]; + Mind?: IToken[]; + SentenceMark: IToken[]; +}; + +export interface StdoutCstNode extends CstNode { + name: "stdout"; + children: StdoutCstChildren; +} + +export type StdoutCstChildren = { + Open?: IToken[]; + SecondPersonPossessive?: IToken[]; + Heart?: IToken[]; + Speak?: IToken[]; + Mind?: IToken[]; + SentenceMark: IToken[]; +}; + +export interface GotoCstNode extends CstNode { + name: "goto"; + children: GotoCstChildren; +} + +export type GotoCstChildren = { + Let?: IToken[]; + Us?: IToken[]; + We?: IToken[]; + Shall?: IToken[]; + Must?: IToken[]; + Return?: IToken[]; + Proceed?: IToken[]; + To: IToken[]; + Act?: IToken[]; + Scene?: IToken[]; + RomanNumeral: IToken[]; + SentenceMark: IToken[]; +}; + +export interface StackPushCstNode extends CstNode { + name: "stackPush"; + children: StackPushCstChildren; +} + +export type StackPushCstChildren = { + Remember: IToken[]; + expression: ExpressionCstNode[]; + SentenceMark: IToken[]; +}; + +export interface StackPopCstNode extends CstNode { + name: "stackPop"; + children: StackPopCstChildren; +} + +export type StackPopCstChildren = { + Recall: IToken[]; + Word?: IToken[]; + SentenceMark: IToken[]; +}; + +export interface QuestionCstNode extends CstNode { + name: "question"; + children: QuestionCstChildren; +} + +export type QuestionCstChildren = { + Be: IToken[]; + lhs: ExpressionCstNode[]; + comparator: ComparatorCstNode[]; + rhs: ExpressionCstNode[]; + QuestionMark: IToken[]; +}; + +export interface ConditionalCstNode extends CstNode { + name: "conditional"; + children: ConditionalCstChildren; +} + +export type ConditionalCstChildren = { + If: IToken[]; + So?: IToken[]; + Not?: IToken[]; + Comma: IToken[]; + nonConditionalDialogueLine: NonConditionalDialogueLineCstNode[]; +}; + +export interface ComparatorCstNode extends CstNode { + name: "comparator"; + children: ComparatorCstChildren; +} + +export type ComparatorCstChildren = { + Not?: IToken[]; + _asComparator?: _asComparatorCstNode[]; + _simpleComparator?: _simpleComparatorCstNode[]; + _moreLessComparator?: _moreLessComparatorCstNode[]; +}; + +export interface _asComparatorCstNode extends CstNode { + name: "_asComparator"; + children: _asComparatorCstChildren; +} + +export type _asComparatorCstChildren = { + As: IToken[]; + adjective: AdjectiveCstNode[]; +}; + +export interface _simpleComparatorCstNode extends CstNode { + name: "_simpleComparator"; + children: _simpleComparatorCstChildren; +} + +export type _simpleComparatorCstChildren = { + comparative: ComparativeCstNode[]; + Than: IToken[]; +}; + +export interface _moreLessComparatorCstNode extends CstNode { + name: "_moreLessComparator"; + children: _moreLessComparatorCstChildren; +} + +export type _moreLessComparatorCstChildren = { + More?: IToken[]; + Less?: IToken[]; + adjective: AdjectiveCstNode[]; + Than: IToken[]; +}; + +export interface ConstantCstNode extends CstNode { + name: "constant"; + children: ConstantCstChildren; +} + +export type ConstantCstChildren = { + _simpleConstant?: _simpleConstantCstNode[]; + _verbalConstant?: _verbalConstantCstNode[]; +}; + +export interface _simpleConstantCstNode extends CstNode { + name: "_simpleConstant"; + children: _simpleConstantCstChildren; +} + +export type _simpleConstantCstChildren = { + Nothing: IToken[]; +}; + +export interface _verbalConstantCstNode extends CstNode { + name: "_verbalConstant"; + children: _verbalConstantCstChildren; +} + +export type _verbalConstantCstChildren = { + Article?: IToken[]; + possessive?: PossessiveCstNode[]; + unarticulatedVerbalConstant: UnarticulatedVerbalConstantCstNode[]; +}; + +export interface UnarticulatedVerbalConstantCstNode extends CstNode { + name: "unarticulatedVerbalConstant"; + children: UnarticulatedVerbalConstantCstChildren; +} + +export type UnarticulatedVerbalConstantCstChildren = { + adjective?: AdjectiveCstNode[]; + noun: NounCstNode[]; +}; + +export interface ExpressionCstNode extends CstNode { + name: "expression"; + children: ExpressionCstChildren; +} + +export type ExpressionCstChildren = { + sumExpression?: SumExpressionCstNode[]; + differenceExpression?: DifferenceExpressionCstNode[]; + productExpression?: ProductExpressionCstNode[]; + quotientExpression?: QuotientExpressionCstNode[]; + remainderExpression?: RemainderExpressionCstNode[]; + factorialExpression?: FactorialExpressionCstNode[]; + squareExpression?: SquareExpressionCstNode[]; + squareRootExpression?: SquareRootExpressionCstNode[]; + cubeExpression?: CubeExpressionCstNode[]; + twiceExpression?: TwiceExpressionCstNode[]; + atomicExpression?: AtomicExpressionCstNode[]; +}; + +export interface AtomicExpressionCstNode extends CstNode { + name: "atomicExpression"; + children: AtomicExpressionCstChildren; +} + +export type AtomicExpressionCstChildren = { + Character?: IToken[]; + FirstPerson?: IToken[]; + SecondPerson?: IToken[]; + reflexive?: ReflexiveCstNode[]; + constant?: ConstantCstNode[]; +}; + +export interface SumExpressionCstNode extends CstNode { + name: "sumExpression"; + children: SumExpressionCstChildren; +} + +export type SumExpressionCstChildren = { + The: IToken[]; + Sum: IToken[]; + Of: IToken[]; + lhs: ExpressionCstNode[]; + And: IToken[]; + rhs: ExpressionCstNode[]; +}; + +export interface DifferenceExpressionCstNode extends CstNode { + name: "differenceExpression"; + children: DifferenceExpressionCstChildren; +} + +export type DifferenceExpressionCstChildren = { + The: IToken[]; + Difference: IToken[]; + Between: IToken[]; + lhs: ExpressionCstNode[]; + And: IToken[]; + rhs: ExpressionCstNode[]; +}; + +export interface ProductExpressionCstNode extends CstNode { + name: "productExpression"; + children: ProductExpressionCstChildren; +} + +export type ProductExpressionCstChildren = { + The: IToken[]; + Product: IToken[]; + Of: IToken[]; + lhs: ExpressionCstNode[]; + And: IToken[]; + rhs: ExpressionCstNode[]; +}; + +export interface QuotientExpressionCstNode extends CstNode { + name: "quotientExpression"; + children: QuotientExpressionCstChildren; +} + +export type QuotientExpressionCstChildren = { + The: IToken[]; + Quotient: IToken[]; + Between: IToken[]; + lhs: ExpressionCstNode[]; + And: IToken[]; + rhs: ExpressionCstNode[]; +}; + +export interface RemainderExpressionCstNode extends CstNode { + name: "remainderExpression"; + children: RemainderExpressionCstChildren; +} + +export type RemainderExpressionCstChildren = { + The: IToken[]; + Remainder: IToken[]; + Of: IToken[]; + Quotient: IToken[]; + Between: IToken[]; + lhs: ExpressionCstNode[]; + And: IToken[]; + rhs: ExpressionCstNode[]; +}; + +export interface FactorialExpressionCstNode extends CstNode { + name: "factorialExpression"; + children: FactorialExpressionCstChildren; +} + +export type FactorialExpressionCstChildren = { + The: IToken[]; + Factorial: IToken[]; + Of: IToken[]; + expression: ExpressionCstNode[]; +}; + +export interface SquareExpressionCstNode extends CstNode { + name: "squareExpression"; + children: SquareExpressionCstChildren; +} + +export type SquareExpressionCstChildren = { + The: IToken[]; + Square: IToken[]; + Of: IToken[]; + expression: ExpressionCstNode[]; +}; + +export interface CubeExpressionCstNode extends CstNode { + name: "cubeExpression"; + children: CubeExpressionCstChildren; +} + +export type CubeExpressionCstChildren = { + The: IToken[]; + Cube: IToken[]; + Of: IToken[]; + expression: ExpressionCstNode[]; +}; + +export interface SquareRootExpressionCstNode extends CstNode { + name: "squareRootExpression"; + children: SquareRootExpressionCstChildren; +} + +export type SquareRootExpressionCstChildren = { + The: IToken[]; + Square: IToken[]; + Root: IToken[]; + Of: IToken[]; + expression: ExpressionCstNode[]; +}; + +export interface TwiceExpressionCstNode extends CstNode { + name: "twiceExpression"; + children: TwiceExpressionCstChildren; +} + +export type TwiceExpressionCstChildren = { + Twice: IToken[]; + expression: ExpressionCstNode[]; +}; + +export interface ICstNodeVisitor extends ICstVisitor { + program(children: ProgramCstChildren, param?: IN): OUT; + programTitle(children: ProgramTitleCstChildren, param?: IN): OUT; + characterIntro(children: CharacterIntroCstChildren, param?: IN): OUT; + actHeading(children: ActHeadingCstChildren, param?: IN): OUT; + sceneHeading(children: SceneHeadingCstChildren, param?: IN): OUT; + entrance(children: EntranceCstChildren, param?: IN): OUT; + exit(children: ExitCstChildren, param?: IN): OUT; + multiExit(children: MultiExitCstChildren, param?: IN): OUT; + entryExitClause(children: EntryExitClauseCstChildren, param?: IN): OUT; + actSection(children: ActSectionCstChildren, param?: IN): OUT; + sceneSection(children: SceneSectionCstChildren, param?: IN): OUT; + sceneSectionChunk(children: SceneSectionChunkCstChildren, param?: IN): OUT; + speakerClause(children: SpeakerClauseCstChildren, param?: IN): OUT; + dialogueSet(children: DialogueSetCstChildren, param?: IN): OUT; + dialogueLine(children: DialogueLineCstChildren, param?: IN): OUT; + nonConditionalDialogueLine(children: NonConditionalDialogueLineCstChildren, param?: IN): OUT; + noun(children: NounCstChildren, param?: IN): OUT; + adjective(children: AdjectiveCstChildren, param?: IN): OUT; + possessive(children: PossessiveCstChildren, param?: IN): OUT; + reflexive(children: ReflexiveCstChildren, param?: IN): OUT; + comparative(children: ComparativeCstChildren, param?: IN): OUT; + assignment(children: AssignmentCstChildren, param?: IN): OUT; + exclaimAssignment(children: ExclaimAssignmentCstChildren, param?: IN): OUT; + arithAssignment(children: ArithAssignmentCstChildren, param?: IN): OUT; + stdin(children: StdinCstChildren, param?: IN): OUT; + stdout(children: StdoutCstChildren, param?: IN): OUT; + goto(children: GotoCstChildren, param?: IN): OUT; + stackPush(children: StackPushCstChildren, param?: IN): OUT; + stackPop(children: StackPopCstChildren, param?: IN): OUT; + question(children: QuestionCstChildren, param?: IN): OUT; + conditional(children: ConditionalCstChildren, param?: IN): OUT; + comparator(children: ComparatorCstChildren, param?: IN): OUT; + _asComparator(children: _asComparatorCstChildren, param?: IN): OUT; + _simpleComparator(children: _simpleComparatorCstChildren, param?: IN): OUT; + _moreLessComparator(children: _moreLessComparatorCstChildren, param?: IN): OUT; + constant(children: ConstantCstChildren, param?: IN): OUT; + _simpleConstant(children: _simpleConstantCstChildren, param?: IN): OUT; + _verbalConstant(children: _verbalConstantCstChildren, param?: IN): OUT; + unarticulatedVerbalConstant(children: UnarticulatedVerbalConstantCstChildren, param?: IN): OUT; + expression(children: ExpressionCstChildren, param?: IN): OUT; + atomicExpression(children: AtomicExpressionCstChildren, param?: IN): OUT; + sumExpression(children: SumExpressionCstChildren, param?: IN): OUT; + differenceExpression(children: DifferenceExpressionCstChildren, param?: IN): OUT; + productExpression(children: ProductExpressionCstChildren, param?: IN): OUT; + quotientExpression(children: QuotientExpressionCstChildren, param?: IN): OUT; + remainderExpression(children: RemainderExpressionCstChildren, param?: IN): OUT; + factorialExpression(children: FactorialExpressionCstChildren, param?: IN): OUT; + squareExpression(children: SquareExpressionCstChildren, param?: IN): OUT; + cubeExpression(children: CubeExpressionCstChildren, param?: IN): OUT; + squareRootExpression(children: SquareRootExpressionCstChildren, param?: IN): OUT; + twiceExpression(children: TwiceExpressionCstChildren, param?: IN): OUT; +} diff --git a/languages/shakespeare/parser/generate-cst-types.ts b/languages/shakespeare/parser/generate-cst-types.ts new file mode 100644 index 0000000..84d3165 --- /dev/null +++ b/languages/shakespeare/parser/generate-cst-types.ts @@ -0,0 +1,18 @@ +import { writeFileSync } from "fs"; +import { resolve } from "path"; +import { generateCstDts } from "chevrotain"; +import { ShakespeareParser } from "./parser"; + +/** + * This script generates CST types for the Shakespeare parser. + * To run: `yarn ts-node $(pwd)/generate-cst-types.ts` in this directory. + * + * The `$(pwd)` makes the path absolute. Due to some reason, relative paths with ts-node + * aren't working on my side. + */ + +const productions = new ShakespeareParser().getGAstProductions(); +const dtsString = generateCstDts(productions); + +const dtsPath = resolve(__dirname, "./cst.d.ts"); +writeFileSync(dtsPath, dtsString); diff --git a/languages/shakespeare/parser/index.ts b/languages/shakespeare/parser/index.ts new file mode 100644 index 0000000..a6fbb69 --- /dev/null +++ b/languages/shakespeare/parser/index.ts @@ -0,0 +1,54 @@ +import { CstNode, IToken, Lexer } from "chevrotain"; +import { DocumentRange } from "../../types"; +import { ParseError } from "../../worker-errors"; +import { ShakespeareParser } from "./parser"; +import { AllTokens } from "./tokens"; +import { ShakespeareVisitor } from "./visitor"; +import { Program } from "./visitor-types"; + +export class Parser { + private readonly _lexer: Lexer = new Lexer(AllTokens); + private readonly _parser: ShakespeareParser = new ShakespeareParser(); + private readonly _visitor: ShakespeareVisitor = new ShakespeareVisitor(); + + public parse(text: string): Program { + const tokens = this.runLexer(text); + const cst = this.runParser(tokens); + return this.runVisitor(cst); + } + + private runLexer(text: string): IToken[] { + const { tokens, errors } = this._lexer.tokenize(text); + if (errors.length > 0) { + const error = errors[0]; + throw new ParseError(error.message, { + startLine: error.line ? error.line - 1 : 0, + startCol: error.column && error.column - 1, + endCol: error.column && error.column + error.length - 1, + }); + } + return tokens; + } + + private runParser(tokens: IToken[]): CstNode { + this._parser.input = tokens; + const parseResult = this._parser.program(); + if (this._parser.errors.length > 0) { + const error = this._parser.errors[0]; + throw new ParseError(error.message, this.getRange(error.token)); + } + + return parseResult; + } + + private runVisitor(cst: CstNode): Program { + return this._visitor.visit(cst); + } + + private getRange(token: IToken): DocumentRange { + const startLine = (token.startLine || 1) - 1; + const startCol = token.startColumn && token.startColumn - 1; + const endCol = token.endColumn; + return { startLine, startCol, endCol }; + } +} diff --git a/languages/shakespeare/parser/parser.ts b/languages/shakespeare/parser/parser.ts new file mode 100644 index 0000000..831c969 --- /dev/null +++ b/languages/shakespeare/parser/parser.ts @@ -0,0 +1,589 @@ +import { CstNode, CstParser, ParserMethod } from "chevrotain"; +import * as Tokens from "./tokens"; + +export interface RuleBook { + program: T; + programTitle: T; + characterIntro: T; + actHeading: T; + sceneHeading: T; + entrance: T; + exit: T; + multiExit: T; + entryExitClause: T; + actSection: T; + sceneSection: T; + sceneSectionChunk: T; + speakerClause: T; + dialogueSet: T; + dialogueLine: T; + // ENGLISH + noun: T; + adjective: T; + possessive: T; + reflexive: T; + comparative: T; + // STATEMENTS + assignment: T; + exclaimAssignment: T; + arithAssignment: T; + stdin: T; + stdout: T; + goto: T; + question: T; + conditional: T; + comparator: T; + _asComparator: T; + _simpleComparator: T; + _moreLessComparator: T; + // CONSTANTS + constant: T; + simpleConstant: T; + verbalConstant: T; + unarticulatedVerbalConstant: T; + // EXPRESSION TREE + expression: T; + atomicExpression: T; + sumExpression: T; + differenceExpression: T; + productExpression: T; + quotientExpression: T; + remainderExpression: T; + factorialExpression: T; + squareExpression: T; + cubeExpression: T; + squareRootExpression: T; + twiceExpression: T; +} + +export class ShakespeareParser + extends CstParser + implements RuleBook> +{ + constructor() { + super(Tokens.AllTokens, { + nodeLocationTracking: "full", + recoveryEnabled: true, + }); + this.performSelfAnalysis(); + } + + program = this.RULE("program", () => { + this.SUBRULE(this.programTitle); + this.MANY(() => this.CONSUME(Tokens.WhitespaceOrNewline)); + this.MANY1(() => this.SUBRULE(this.characterIntro)); + this.MANY2(() => this.SUBRULE(this.actSection)); + }); + + /** */ + programTitle = this.RULE("programTitle", () => { + this.MANY(() => this.CONSUME(Tokens.Word)); + this.CONSUME(Tokens.Period); + }); + + /** `, ` */ + characterIntro = this.RULE("characterIntro", () => { + this.CONSUME(Tokens.Character); + this.CONSUME(Tokens.Comma); + this.MANY(() => this.CONSUME(Tokens.Word)); + this.CONSUME(Tokens.SentenceMark); + }); + + /** `Act : ` */ + actHeading = this.RULE("actHeading", () => { + this.CONSUME(Tokens.Act); + this.CONSUME(Tokens.RomanNumeral); + this.CONSUME(Tokens.Colon); + this.MANY(() => this.CONSUME(Tokens.Word)); + this.CONSUME(Tokens.SentenceMark); + }); + + /** `Scene : ` */ + sceneHeading = this.RULE("sceneHeading", () => { + this.CONSUME(Tokens.Scene); + this.CONSUME(Tokens.RomanNumeral); + this.CONSUME(Tokens.Colon); + this.MANY(() => this.CONSUME(Tokens.Word)); + this.CONSUME(Tokens.SentenceMark); + }); + + /** `[Enter (and )]` */ + entrance = this.RULE("entrance", () => { + this.CONSUME(Tokens.SquareBracketOpen); + this.CONSUME(Tokens.Enter); + this.CONSUME1(Tokens.Character); + this.OPTION(() => { + this.CONSUME(Tokens.And); + this.CONSUME2(Tokens.Character); + }); + this.CONSUME(Tokens.SquareBracketClose); + }); + + /** `[Exit ]` */ + exit = this.RULE("exit", () => { + this.CONSUME(Tokens.SquareBracketOpen); + this.CONSUME(Tokens.Exit); + this.CONSUME(Tokens.Character); + this.CONSUME(Tokens.SquareBracketClose); + }); + + /** `[Exeunt ( and )]` */ + multiExit = this.RULE("multiExit", () => { + this.CONSUME(Tokens.SquareBracketOpen); + this.CONSUME(Tokens.Exeunt); + this.OPTION(() => { + this.CONSUME1(Tokens.Character); + this.CONSUME(Tokens.And); + this.CONSUME2(Tokens.Character); + }); + this.CONSUME(Tokens.SquareBracketClose); + }); + + /** Clause for entry or exit of characters */ + entryExitClause = this.RULE("entryExitClause", () => { + this.OR([ + { ALT: () => this.SUBRULE(this.entrance) }, + { ALT: () => this.SUBRULE(this.exit) }, + { ALT: () => this.SUBRULE(this.multiExit) }, + ]); + }); + + /** Text corresponding to a single act */ + actSection = this.RULE("actSection", () => { + this.SUBRULE(this.actHeading); + this.AT_LEAST_ONE(() => this.SUBRULE(this.sceneSection)); + }); + + /** Text corresponding to a single scene */ + sceneSection = this.RULE("sceneSection", () => { + this.SUBRULE(this.sceneHeading); + this.AT_LEAST_ONE(() => this.SUBRULE(this.sceneSectionChunk)); + }); + + /** A single item of a scene: dialogue set or entry-exit clause */ + sceneSectionChunk = this.RULE("sceneSectionChunk", () => { + this.OR([ + { ALT: () => this.SUBRULE(this.entryExitClause) }, + { ALT: () => this.SUBRULE(this.dialogueSet) }, + ]); + }); + + /** `:` */ + speakerClause = this.RULE("speakerClause", () => { + this.CONSUME(Tokens.Character); + this.CONSUME(Tokens.Colon); + }); + + /** Set of dialogues spoken by a character */ + dialogueSet = this.RULE("dialogueSet", () => { + this.SUBRULE(this.speakerClause); + this.AT_LEAST_ONE(() => this.SUBRULE(this.dialogueLine)); + }); + + /** A single line of dialogue spoken by a character */ + dialogueLine = this.RULE("dialogueLine", () => { + this.OR([ + { ALT: () => this.SUBRULE(this.conditional) }, + { ALT: () => this.SUBRULE(this.nonConditionalDialogueLine) }, + ]); + }); + + /** Dialogue line possibilities, excluding conditionals */ + nonConditionalDialogueLine = this.RULE("nonConditionalDialogueLine", () => { + this.OR([ + { ALT: () => this.SUBRULE(this.assignment) }, + { ALT: () => this.SUBRULE(this.stdin) }, + { ALT: () => this.SUBRULE(this.stdout) }, + { ALT: () => this.SUBRULE(this.goto) }, + { ALT: () => this.SUBRULE(this.stackPush) }, + { ALT: () => this.SUBRULE(this.stackPop) }, + { ALT: () => this.SUBRULE(this.question) }, + ]); + }); + + /////////////////////////////// + ////////// ENGLISH ////////// + /////////////////////////////// + + /** Shakespearean noun */ + noun = this.RULE("noun", () => { + this.OR([ + { ALT: () => this.CONSUME(Tokens.NegativeNoun) }, + { ALT: () => this.CONSUME(Tokens.NeutralNoun) }, + { ALT: () => this.CONSUME(Tokens.PositiveNoun) }, + ]); + }); + + /** Shakesperean adjective */ + adjective = this.RULE("adjective", () => { + this.OR([ + { ALT: () => this.CONSUME(Tokens.NegativeAdjective) }, + { ALT: () => this.CONSUME(Tokens.NeutralAdjective) }, + { ALT: () => this.CONSUME(Tokens.PositiveAdjective) }, + ]); + }); + + /** Any recognized possessive (my, your, his, her, etc.) */ + possessive = this.RULE("possessive", () => { + this.OR([ + { ALT: () => this.CONSUME(Tokens.FirstPersonPossessive) }, + { ALT: () => this.CONSUME(Tokens.SecondPersonPossessive) }, + { ALT: () => this.CONSUME(Tokens.ThirdPersonPossessive) }, + ]); + }); + + /** Any recognized reflexive (myself, thyself, etc.) */ + reflexive = this.RULE("reflexive", () => { + this.OR([ + { ALT: () => this.CONSUME(Tokens.FirstPersonReflexive) }, + { ALT: () => this.CONSUME(Tokens.SecondPersonReflexive) }, + ]); + }); + + /** Any recognized comparative (better, punier, etc) */ + comparative = this.RULE("comparative", () => { + this.OR([ + { ALT: () => this.CONSUME(Tokens.PositiveComparative) }, + { ALT: () => this.CONSUME(Tokens.NegativeComparative) }, + ]); + }); + + /////////////////////////////// + ///////// STATEMENTS //////// + /////////////////////////////// + + /** Assignment of an expression to a character */ + assignment = this.RULE("assignment", () => { + this.OR([ + { ALT: () => this.SUBRULE(this.exclaimAssignment) }, + { ALT: () => this.SUBRULE(this.arithAssignment) }, + ]); + }); + + /** ` ` */ + exclaimAssignment = this.RULE("exclaimAssignment", () => { + this.CONSUME(Tokens.SecondPerson); + this.SUBRULE(this.unarticulatedVerbalConstant); + this.CONSUME(Tokens.SentenceMark); + }); + + /** ` as as ` */ + arithAssignment = this.RULE("arithAssignment", () => { + this.CONSUME(Tokens.SecondPerson); + this.CONSUME(Tokens.Be); + this.OPTION(() => { + this.CONSUME1(Tokens.As); + this.SUBRULE(this.adjective); + this.CONSUME2(Tokens.As); + }); + this.SUBRULE(this.expression); + this.CONSUME(Tokens.SentenceMark); + }); + + /** `Listen to your heart | Open your mind` */ + stdin = this.RULE("stdin", () => { + this.OR([ + { + ALT: () => { + this.CONSUME(Tokens.Listen); + this.CONSUME(Tokens.To); + this.CONSUME1(Tokens.SecondPersonPossessive); + this.CONSUME(Tokens.Heart); + }, + }, + { + ALT: () => { + this.CONSUME(Tokens.Open); + this.CONSUME2(Tokens.SecondPersonPossessive); + this.CONSUME(Tokens.Mind); + }, + }, + ]); + this.CONSUME(Tokens.SentenceMark); + }); + + /** `Open heart | Speak mind` */ + stdout = this.RULE("stdout", () => { + this.OR([ + { + ALT: () => { + this.CONSUME(Tokens.Open); + this.CONSUME1(Tokens.SecondPersonPossessive); + this.CONSUME(Tokens.Heart); + }, + }, + { + ALT: () => { + this.CONSUME(Tokens.Speak); + this.CONSUME2(Tokens.SecondPersonPossessive); + this.CONSUME(Tokens.Mind); + }, + }, + ]); + this.CONSUME(Tokens.SentenceMark); + }); + + /** (let us|we shall|we must) (return|proceed) to (act|scene) */ + goto = this.RULE("goto", () => { + this.OR1([ + { + ALT: () => { + this.CONSUME(Tokens.Let); + this.CONSUME(Tokens.Us); + }, + }, + { + ALT: () => { + this.CONSUME1(Tokens.We); + this.CONSUME(Tokens.Shall); + }, + }, + { + ALT: () => { + this.CONSUME2(Tokens.We); + this.CONSUME(Tokens.Must); + }, + }, + ]); + this.OR2([ + { ALT: () => this.CONSUME(Tokens.Return) }, + { ALT: () => this.CONSUME(Tokens.Proceed) }, + ]); + this.CONSUME(Tokens.To); + this.OR3([ + { ALT: () => this.CONSUME(Tokens.Act) }, + { ALT: () => this.CONSUME(Tokens.Scene) }, + ]); + this.CONSUME(Tokens.RomanNumeral); + this.CONSUME(Tokens.SentenceMark); + }); + + /** `Remember ` */ + stackPush = this.RULE("stackPush", () => { + this.CONSUME(Tokens.Remember); + this.SUBRULE(this.expression); + this.CONSUME(Tokens.SentenceMark); + }); + + /** `Recall *` */ + stackPop = this.RULE("stackPop", () => { + this.CONSUME(Tokens.Recall); + this.MANY(() => this.CONSUME(Tokens.Word)); + this.CONSUME(Tokens.SentenceMark); + }); + + /** ` ?` */ + question = this.RULE("question", () => { + this.CONSUME(Tokens.Be); + this.SUBRULE1(this.expression, { LABEL: "lhs" }); + this.SUBRULE(this.comparator); + this.SUBRULE2(this.expression, { LABEL: "rhs" }); + this.CONSUME(Tokens.QuestionMark); + }); + + /** `If so, ` */ + conditional = this.RULE("conditional", () => { + this.CONSUME(Tokens.If); + this.OR([ + { ALT: () => this.CONSUME(Tokens.So) }, + { ALT: () => this.CONSUME(Tokens.Not) }, + ]); + this.CONSUME(Tokens.Comma); + this.SUBRULE(this.nonConditionalDialogueLine); + }); + + /** Comparator clause used in questions */ + comparator = this.RULE("comparator", () => { + this.OPTION(() => this.CONSUME(Tokens.Not)); + this.OR([ + { ALT: () => this.SUBRULE(this._asComparator) }, + { ALT: () => this.SUBRULE(this._simpleComparator) }, + { ALT: () => this.SUBRULE(this._moreLessComparator) }, + ]); + }); + + /** `as as` */ + _asComparator = this.RULE("_asComparator", () => { + this.CONSUME1(Tokens.As); + this.SUBRULE(this.adjective); + this.CONSUME2(Tokens.As); + }); + + /** ` than` */ + _simpleComparator = this.RULE("_simpleComparator", () => { + this.SUBRULE(this.comparative); + this.CONSUME(Tokens.Than); + }); + + /** `(more|less) than` */ + _moreLessComparator = this.RULE("_moreLessComparator", () => { + this.OR([ + { ALT: () => this.CONSUME(Tokens.More) }, + { ALT: () => this.CONSUME(Tokens.Less) }, + ]); + this.SUBRULE(this.adjective); + this.CONSUME(Tokens.Than); + }); + + /////////////////////////////// + ///////// CONSTANTS ///////// + /////////////////////////////// + + /** Constant expressions */ + constant = this.RULE("constant", () => { + this.OR([ + { ALT: () => this.SUBRULE(this.simpleConstant) }, + { ALT: () => this.SUBRULE(this.verbalConstant) }, + ]); + }); + + /** Simple keyword-based constant */ + simpleConstant = this.RULE("_simpleConstant", () => { + this.CONSUME(Tokens.Nothing); + }); + + /** Verbally expressed constant */ + verbalConstant = this.RULE("_verbalConstant", () => { + this.OR([ + { ALT: () => this.CONSUME(Tokens.Article) }, + { ALT: () => this.SUBRULE(this.possessive) }, + ]); + this.SUBRULE(this.unarticulatedVerbalConstant); + }); + + /** `* `, representing a constant */ + unarticulatedVerbalConstant = this.RULE("unarticulatedVerbalConstant", () => { + // Shakespeare only allows non-negative adjectives on positive nouns and + // negative adjectives on negative nouns. + // + // Unfortunately, positive and negative branches cannot be separated in this + // parser since they share an arbitrarily long prefix of neutral adjectives. + // Thus, this branch validation must be done in the CST visitor. + this.MANY(() => this.SUBRULE(this.adjective)); + this.SUBRULE(this.noun); + }); + + /////////////////////////////// + ////// EXPRESSION TREE ////// + /////////////////////////////// + + /** Root node of an expression tree */ + expression = this.RULE("expression", () => { + this.OR([ + { ALT: () => this.SUBRULE(this.sumExpression) }, + { ALT: () => this.SUBRULE(this.differenceExpression) }, + { ALT: () => this.SUBRULE(this.productExpression) }, + { ALT: () => this.SUBRULE(this.quotientExpression) }, + { ALT: () => this.SUBRULE(this.remainderExpression) }, + { ALT: () => this.SUBRULE(this.factorialExpression) }, + { ALT: () => this.SUBRULE(this.squareExpression) }, + { ALT: () => this.SUBRULE(this.squareRootExpression) }, + { ALT: () => this.SUBRULE(this.cubeExpression) }, + { ALT: () => this.SUBRULE(this.twiceExpression) }, + { ALT: () => this.SUBRULE(this.atomicExpression) }, + ]); + }); + + /** ` | | ` */ + atomicExpression = this.RULE("atomicExpression", () => { + this.OR([ + { ALT: () => this.CONSUME(Tokens.Character) }, + { ALT: () => this.CONSUME(Tokens.FirstPerson) }, + { ALT: () => this.CONSUME(Tokens.SecondPerson) }, + { ALT: () => this.SUBRULE(this.reflexive) }, + { ALT: () => this.SUBRULE(this.constant) }, + ]); + }); + + /** `the sum of and ` */ + sumExpression = this.RULE("sumExpression", () => { + this.CONSUME(Tokens.The); + this.CONSUME(Tokens.Sum); + this.CONSUME(Tokens.Of); + this.SUBRULE1(this.expression, { LABEL: "lhs" }); + this.CONSUME(Tokens.And); + this.SUBRULE2(this.expression, { LABEL: "rhs" }); + }); + + /** `the difference between and ` */ + differenceExpression = this.RULE("differenceExpression", () => { + this.CONSUME(Tokens.The); + this.CONSUME(Tokens.Difference); + this.CONSUME(Tokens.Between); + this.SUBRULE1(this.expression, { LABEL: "lhs" }); + this.CONSUME(Tokens.And); + this.SUBRULE2(this.expression, { LABEL: "rhs" }); + }); + + /** `the product of and ` */ + productExpression = this.RULE("productExpression", () => { + this.CONSUME(Tokens.The); + this.CONSUME(Tokens.Product); + this.CONSUME(Tokens.Of); + this.SUBRULE1(this.expression, { LABEL: "lhs" }); + this.CONSUME(Tokens.And); + this.SUBRULE2(this.expression, { LABEL: "rhs" }); + }); + + /** `the quotient between and ` */ + quotientExpression = this.RULE("quotientExpression", () => { + this.CONSUME(Tokens.The); + this.CONSUME(Tokens.Quotient); + this.CONSUME(Tokens.Between); + this.SUBRULE1(this.expression, { LABEL: "lhs" }); + this.CONSUME(Tokens.And); + this.SUBRULE2(this.expression, { LABEL: "rhs" }); + }); + + /** `the remainder of the quotient between and ` */ + remainderExpression = this.RULE("remainderExpression", () => { + this.CONSUME1(Tokens.The); + this.CONSUME(Tokens.Remainder); + this.CONSUME(Tokens.Of); + this.CONSUME2(Tokens.The); + this.CONSUME(Tokens.Quotient); + this.CONSUME(Tokens.Between); + this.SUBRULE1(this.expression, { LABEL: "lhs" }); + this.CONSUME(Tokens.And); + this.SUBRULE2(this.expression, { LABEL: "rhs" }); + }); + + /** `the factorial of ` */ + factorialExpression = this.RULE("factorialExpression", () => { + this.CONSUME(Tokens.The); + this.CONSUME(Tokens.Factorial); + this.CONSUME(Tokens.Of); + this.SUBRULE(this.expression); + }); + + /** `the square of ` */ + squareExpression = this.RULE("squareExpression", () => { + this.CONSUME(Tokens.The); + this.CONSUME(Tokens.Square); + this.CONSUME(Tokens.Of); + this.SUBRULE(this.expression); + }); + + /** `the cube of ` */ + cubeExpression = this.RULE("cubeExpression", () => { + this.CONSUME(Tokens.The); + this.CONSUME(Tokens.Cube); + this.CONSUME(Tokens.Of); + this.SUBRULE(this.expression); + }); + + /** `the square root of ` */ + squareRootExpression = this.RULE("squareRootExpression", () => { + this.CONSUME(Tokens.The); + this.CONSUME(Tokens.Square); + this.CONSUME(Tokens.Root); + this.CONSUME(Tokens.Of); + this.SUBRULE(this.expression); + }); + + /** `twice ` */ + twiceExpression = this.RULE("twiceExpression", () => { + this.CONSUME(Tokens.Twice); + this.SUBRULE(this.expression); + }); +} diff --git a/languages/shakespeare/parser/tokens.ts b/languages/shakespeare/parser/tokens.ts new file mode 100644 index 0000000..380c7a1 --- /dev/null +++ b/languages/shakespeare/parser/tokens.ts @@ -0,0 +1,509 @@ +import { createToken, Lexer } from "chevrotain"; +import * as R from "./constants"; + +export const Word = createToken({ + name: "Word", + pattern: /[^\s\n\.\?\!]+/, +}); + +export const WhitespaceOrNewline = createToken({ + name: "WhitespaceOrNewline", + pattern: /[\s\n]+/, + group: Lexer.SKIPPED, +}); + +export const Whitespace = createToken({ + name: "Whitespace", + pattern: /\s+/, + categories: [WhitespaceOrNewline], + group: Lexer.SKIPPED, +}); + +export const RomanNumeral = createToken({ + name: "RomanNumeral", + pattern: /[IVXLCDM]+\b/i, + categories: [Word], +}); + +export const Enter = createToken({ + name: "Enter", + pattern: /enter\b/i, + categories: [Word], +}); + +export const Exit = createToken({ + name: "Exit", + pattern: /exit\b/i, + categories: [Word], +}); + +export const Exeunt = createToken({ + name: "Exeunt", + pattern: /exeunt\b/i, + categories: [Word], +}); + +export const And = createToken({ + name: "And", + pattern: /and\b/i, + categories: [Word], +}); + +export const Article = createToken({ + name: "Article", + pattern: /(a|an|the)\b/i, + categories: [Word], +}); + +export const Be = createToken({ + name: "Be", + pattern: /(am|are|art|be|is)\b/i, + categories: [Word], +}); + +export const Not = createToken({ + name: "Not", + pattern: /not\b/i, + categories: [Word], +}); + +export const More = createToken({ + name: "More", + pattern: /more\b/i, + categories: [Word], +}); + +export const Less = createToken({ + name: "Less", + pattern: /less\b/i, + categories: [Word], +}); + +export const Than = createToken({ + name: "Than", + pattern: /than\b/i, + categories: [Word], +}); + +export const Let = createToken({ + name: "Let", + pattern: /let\b/i, + categories: [Word], +}); + +export const Us = createToken({ + name: "Us", + pattern: /us\b/i, + categories: [Word], +}); + +export const We = createToken({ + name: "We", + pattern: /we\b/i, + categories: [Word], +}); + +export const Shall = createToken({ + name: "Shall", + pattern: /shall\b/i, + categories: [Word], +}); + +export const Must = createToken({ + name: "Must", + pattern: /must\b/i, + categories: [Word], +}); + +export const Return = createToken({ + name: "Return", + pattern: /return\b/i, + categories: [Word], +}); + +export const Proceed = createToken({ + name: "Proceed", + pattern: /proceed\b/i, + categories: [Word], +}); + +export const Character = createToken({ + name: "Character", + pattern: R.CharacterRegex, + categories: [Word], +}); + +export const FirstPersonPossessive = createToken({ + name: "FirstPersonPossessive", + pattern: /(mine|my)\b/i, + categories: [Word], +}); + +export const FirstPersonReflexive = createToken({ + name: "FirstPersonReflexive", + pattern: /myself\b/i, + categories: [Word], +}); + +export const FirstPerson = createToken({ + name: "FirstPerson", + pattern: /(i|me)\b/i, + categories: [Word], +}); + +export const NegativeAdjective = createToken({ + name: "NegativeAdjective", + pattern: R.NegativeAdjectiveRegex, + categories: [Word], +}); + +export const NegativeComparative = createToken({ + name: "NegativeComparative", + pattern: /(punier|smaller|worse)\b/i, + categories: [Word], +}); + +export const NegativeNoun = createToken({ + name: "NegativeNoun", + pattern: R.NegativeNounRegex, + categories: [Word], +}); + +export const NeutralAdjective = createToken({ + name: "NeutralAdjective", + pattern: R.NeutralAdjectiveRegex, + categories: [Word], +}); + +export const NeutralNoun = createToken({ + name: "NeutralNoun", + pattern: R.NeutralNounRegex, + categories: [Word], +}); + +export const Nothing = createToken({ + name: "Nothing", + pattern: /(nothing|zero)\b/i, + categories: [Word], +}); + +export const PositiveAdjective = createToken({ + name: "PositiveAdjective", + pattern: R.PositiveAdjectiveRegex, + categories: [Word], +}); + +export const PositiveComparative = createToken({ + name: "PositiveComparative", + pattern: /(better|bigger|fresher|friendlier|nicer|jollier)\b/i, + categories: [Word], +}); + +export const PositiveNoun = createToken({ + name: "PositiveNoun", + pattern: R.PositiveNounRegex, + categories: [Word], +}); + +export const SecondPersonPossessive = createToken({ + name: "SecondPersonPossessive", + pattern: /(thine|thy|your)\b/i, + categories: [Word], +}); + +export const SecondPersonReflexive = createToken({ + name: "SecondPersonReflexive", + pattern: /(thyself|yourself)\b/i, + categories: [Word], +}); + +export const SecondPerson = createToken({ + name: "SecondPerson", + pattern: /(thee|thou|you)\b/i, + categories: [Word], +}); + +export const ThirdPersonPossessive = createToken({ + name: "ThirdPersonPossessive", + pattern: /(his|her|its|their)\b/i, + categories: [Word], +}); + +export const Act = createToken({ + name: "Act", + pattern: /act\b/i, + categories: [Word], +}); + +export const Scene = createToken({ + name: "Scene", + pattern: /scene\b/i, + categories: [Word], +}); + +export const As = createToken({ + name: "As", + pattern: /as\b/i, + categories: [Word], +}); + +export const Open = createToken({ + name: "Open", + pattern: /open\b/i, + categories: [Word], +}); + +export const Heart = createToken({ + name: "Heart", + pattern: /heart\b/i, + categories: [Word], +}); + +export const Speak = createToken({ + name: "Speak", + pattern: /speak\b/i, + categories: [Word], +}); + +export const Mind = createToken({ + name: "Mind", + pattern: /mind\b/i, + categories: [Word], +}); + +export const The = createToken({ + name: "The", + pattern: /the\b/i, + categories: [Word, Article], +}); + +export const Between = createToken({ + name: "Between", + pattern: /between\b/i, + categories: [Word], +}); + +export const Of = createToken({ + name: "Of", + pattern: /of\b/i, + categories: [Word], +}); + +export const If = createToken({ + name: "If", + pattern: /if\b/i, + categories: [Word], +}); + +export const So = createToken({ + name: "So", + pattern: /so\b/i, + categories: [Word], +}); + +export const To = createToken({ + name: "To", + pattern: /to\b/i, + categories: [Word], +}); + +export const Listen = createToken({ + name: "Listen", + pattern: /listen\b/i, + categories: [Word], +}); + +export const Recall = createToken({ + name: "Recall", + pattern: /recall\b/i, + categories: [Word], +}); + +export const Remember = createToken({ + name: "Remember", + pattern: /remember\b/i, + categories: [Word], +}); + +//////////////////////////////// +/////////// OPERATORS ////////// +//////////////////////////////// + +export const Sum = createToken({ + name: "Sum", + pattern: /sum\b/, + categories: [Word], +}); + +export const Difference = createToken({ + name: "Difference", + pattern: /difference\b/, + categories: [Word], +}); + +export const Product = createToken({ + name: "Product", + pattern: /product\b/i, + categories: [Word], +}); + +export const Quotient = createToken({ + name: "Quotient", + pattern: /quotient\b/i, + categories: [Word], +}); + +export const Remainder = createToken({ + name: "Remainder", + pattern: /remainder\b/i, + categories: [Word], +}); + +export const Factorial = createToken({ + name: "Factorial", + pattern: /factorial\b/i, + categories: [Word], +}); + +export const Square = createToken({ + name: "Square", + pattern: /square\b/i, + categories: [Word], +}); + +export const Root = createToken({ + name: "Root", + pattern: /root\b/i, + categories: [Word], +}); + +export const Cube = createToken({ + name: "Cube", + pattern: /cube\b/i, + categories: [Word], +}); + +export const Twice = createToken({ + name: "Twice", + pattern: /twice\b/i, + categories: [Word], +}); + +export const SentenceMark = createToken({ + name: "SentenceMark", + pattern: /\.|\?|\!/, +}); + +export const SquareBracketOpen = createToken({ + name: "SquareBracketOpen", + pattern: /\[/, +}); + +export const SquareBracketClose = createToken({ + name: "SquareBracketClose", + pattern: /\]/, +}); + +export const Colon = createToken({ + name: "Colon", + pattern: /:/, +}); + +export const Comma = createToken({ + name: "Comma", + pattern: /,/, + categories: [Word], +}); + +export const Period = createToken({ + name: "Period", + pattern: /\./, + categories: [SentenceMark], +}); + +export const QuestionMark = createToken({ + name: "QuestionMark", + pattern: /\?/, + categories: [SentenceMark], +}); + +export const I = createToken({ + name: "I", + pattern: /i\b/i, + categories: [RomanNumeral, FirstPerson, Word], +}); + +export const AllTokens = [ + Whitespace, + WhitespaceOrNewline, + Character, + The, + Be, + More, + Less, + Than, + Let, + Us, + We, + Shall, + Must, + Return, + Proceed, + Not, + As, + I, + If, + So, + To, + Enter, + Exit, + Exeunt, + Open, + Heart, + Speak, + Mind, + Listen, + Recall, + Remember, + And, + Nothing, + Between, + Of, + Sum, + Difference, + Product, + Quotient, + Remainder, + Factorial, + Square, + Root, + Cube, + Twice, + Period, + QuestionMark, + Article, + Act, + Scene, + Comma, + Colon, + SquareBracketOpen, + SquareBracketClose, + SentenceMark, + FirstPersonPossessive, + FirstPersonReflexive, + FirstPerson, + NegativeAdjective, + NegativeComparative, + NegativeNoun, + NeutralAdjective, + NeutralNoun, + PositiveAdjective, + PositiveComparative, + PositiveNoun, + SecondPersonPossessive, + SecondPersonReflexive, + SecondPerson, + ThirdPersonPossessive, + RomanNumeral, + Word, +]; diff --git a/languages/shakespeare/parser/visitor-types.d.ts b/languages/shakespeare/parser/visitor-types.d.ts new file mode 100644 index 0000000..1accc32 --- /dev/null +++ b/languages/shakespeare/parser/visitor-types.d.ts @@ -0,0 +1,323 @@ +import { DocumentRange } from "../../../types"; +import * as C from "./cst"; + +/** Type that contains information about what AST the visitor returns for each rule */ +export type ShakespeareVisitorTypes = { + program(children: C.ProgramCstChildren, param?: any): Program; + programTitle(children: C.ProgramTitleCstChildren, param?: any): null; + characterIntro( + children: C.CharacterIntroCstChildren, + param?: any + ): CharacterIntro; + actHeading(children: C.ActHeadingCstChildren, param?: any): ActHeading; + sceneHeading(children: C.SceneHeadingCstChildren, param?: any): SceneHeading; + entrance(children: C.EntranceCstChildren, param?: any): EntryExitClause; + exit(children: C.ExitCstChildren, param?: any): EntryExitClause; + multiExit(children: C.MultiExitCstChildren, param?: any): EntryExitClause; + entryExitClause( + children: C.EntryExitClauseCstChildren, + param?: any + ): EntryExitClause; + actSection(children: C.ActSectionCstChildren, param?: any): ActSection; + sceneSection(children: C.SceneSectionCstChildren, param?: any): SceneSection; + sceneSectionChunk( + children: C.SceneSectionChunkCstChildren, + param?: any + ): SceneSectionChunk; + speakerClause( + children: C.SpeakerClauseCstChildren, + param?: any + ): SpeakerClause; + dialogueSet(children: C.DialogueSetCstChildren, param?: any): DialogueSet; + dialogueLine(children: C.DialogueCstChildren, param?: any): DialogueLine; + nonConditionalDialogueLine( + children: C.NonConditionalDialogueLineCstChildren, + param?: any + ): NonConditionalDialogueLine; + + /////////// ENGLISH /////////// + noun(children: C.NounCstChildren, param?: any): 1 | 0 | -1; + adjective(children: C.AdjectiveCstChildren, param?: any): 1 | 0 | -1; + possessive( + children: C.PossessiveCstChildren, + param?: any + ): "first" | "second" | "third"; + reflexive(children: C.ReflexiveCstChildren, param?: any): "first" | "second"; + comparative(children: C.ComparativeCstChildren, param?: any): 1 | -1; + + /////////// STATEMENTS /////////// + assignment(children: C.AssignmentCstChildren, param?: any): AssignmentLine; + exclaimAssignment( + children: C.ExclaimAssignmentCstChildren, + param?: any + ): AssignmentLine; + arithAssignment( + children: C.ArithAssignmentCstChildren, + param?: any + ): AssignmentLine; + stdin(children: C.StdinCstChildren, param?: any): StdinLine; + stdout(children: C.StdoutCstChildren, param?: any): StdoutLine; + goto(children: C.GotoCstChildren, param?: any): GotoLine; + stackPush(children: C.StackPushCstChildren, param?: any): StackPushLine; + stackPop(children: C.StackPopCstChildren, param?: any): StackPopLine; + question(children: C.QuestionCstChildren, param?: any): QuestionLine; + conditional(children: C.ConditionalCstChildren, param?: any): ConditionalLine; + comparator( + children: C.ComparatorCstChildren, + param?: any + ): ComparisonOperator; + _asComparator( + children: C._asComparatorCstChildren, + param?: any + ): ComparisonOperator; + _simpleComparator( + children: C._simpleComparatorCstChildren, + param?: any + ): ComparisonOperator; + _moreLessComparator( + children: C._moreLessComparatorCstChildren, + param?: any + ): ComparisonOperator; + + ////////////// CONSTANTS ////////////// + constant(children: C.ConstantCstChildren, param?: any): Constant; + _simpleConstant( + children: C._simpleConstantCstChildren, + param?: any + ): Constant; + _verbalConstant( + children: C._verbalConstantCstChildren, + param?: any + ): Constant; + unarticulatedVerbalConstant( + children: C.UnarticulatedVerbalConstantCstChildren, + param?: any + ): Constant; + + /////////// EXPRESSION TREE /////////// + expression(children: C.ExpressionCstChildren, param?: any): Expression; + atomicExpression( + children: C.AtomicExpressionCstChildren, + param?: any + ): AtomicExpression; + sumExpression( + children: C.SumExpressionCstChildren, + param?: any + ): BinaryExpression; + differenceExpression( + children: C.DifferenceExpressionCstChildren, + param?: any + ): BinaryExpression; + productExpression( + children: C.ProductExpressionCstChildren, + param?: any + ): BinaryExpression; + quotientExpression( + children: C.QuotientExpressionCstChildren, + param?: any + ): BinaryExpression; + remainderExpression( + children: C.RemainderExpressionCstChildren, + param?: any + ): BinaryExpression; + factorialExpression( + children: C.FactorialExpressionCstChildren, + param?: any + ): UnaryExpression; + squareExpression( + children: C.SquareExpressionCstChildren, + param?: any + ): UnaryExpression; + cubeExpression( + children: C.CubeExpressionCstChildren, + param?: any + ): UnaryExpression; + squareRootExpression( + children: C.SquareRootExpressionCstChildren, + param?: any + ): UnaryExpression; + twiceExpression( + children: C.TwiceExpressionCstChildren, + param?: any + ): UnaryExpression; +}; + +/** We don't parse roman numerals and instead just treat them as string IDs */ +export type RomanNumeral = string; + +/** AST for a complete Shakespeare program */ +export type Program = { + characters: CharacterIntro[]; + acts: ActSection[]; +}; + +/** Character declaration at the start of a program */ +export type CharacterIntro = Character; + +/** Heading of an act - only the act number is kept */ +export type ActHeading = RomanNumeral; +/** Heading of a scene - only the scene number is kept */ +export type SceneHeading = RomanNumeral; + +/** Clause for entry or exit of one or more characters */ +export type EntryExitClause = { + type: "entry" | "exit"; + /** + * List of characters entering or leaving. + * `null` is used for `[Exeunt]` clauses. + */ + characters: Character[] | null; + range: DocumentRange; +}; + +/** Details of a single act */ +export type ActSection = { + id: ActHeading; + scenes: SceneSection[]; +}; + +/** Details of a single scene */ +export type SceneSection = { + id: SceneHeading; + items: SceneSectionItem[]; +}; + +/** An execution atom of a single scene */ +export type SceneSectionItem = EntryExitClause | DialogueItem; + +/** A dialogue set or entry-exit clause belonging to a scene */ +export type SceneSectionChunk = EntryExitClause | DialogueSet; + +/** `:` clause that starts a dialogue set */ +export type SpeakerClause = Character; + +/** Set of dialogues spoken by a character */ +export type DialogueSet = { + type: "dialogue-set"; + speaker: SpeakerClause; + lines: DialogueLine[]; +}; + +/** A single dialogue item containing speaker and line */ +export type DialogueItem = { + type: "dialogue-item"; + speaker: Character; + line: DialogueLine; +}; + +/** Single dialogue line spoken by the current character */ +export type DialogueLine = NonConditionalDialogueLine | ConditionalLine; + +/** Dialogue line excluding conditional */ +export type NonConditionalDialogueLine = + | AssignmentLine + | StdinLine + | StdoutLine + | GotoLine + | StackPushLine + | StackPopLine + | QuestionLine; + +/** Dialogue line representing an assignment operation */ +export type AssignmentLine = { + type: "assign"; + value: Expression; + range: DocumentRange; +}; + +/** Dialogue line representing an STDOUT operation */ +export type StdoutLine = { + type: "stdout"; + outType: "num" | "char"; + range: DocumentRange; +}; + +/** Dialogue line representing an STDIN operation */ +export type StdinLine = { + type: "stdin"; + inType: "num" | "char"; + range: DocumentRange; +}; + +/** Dialogue line representing a goto operation */ +export type GotoLine = { + type: "goto"; + targetType: "act" | "scene"; + target: RomanNumeral; + range: DocumentRange; +}; + +/** Dialogue line representing a stack push */ +export type StackPushLine = { + type: "stack-push"; + expr: Expression; + range: DocumentRange; +}; + +/** Dialogue line representing a stack pop */ +export type StackPopLine = { + type: "stack-pop"; + range: DocumentRange; +}; + +/** Dialogue line representing a question */ +export type QuestionLine = { + type: "question"; + comparator: ComparisonOperator; + lhs: Expression; + rhs: Expression; + range: DocumentRange; +}; + +/** Dialogue line representing a conditional (`If so/not, ...`) */ +export type ConditionalLine = { + type: "conditional"; + invert: boolean; + consequent: NonConditionalDialogueLine; + range: DocumentRange; +}; + +/** Comparison operator used in a question */ +export type ComparisonOperator = { + invert?: boolean; + type: "==" | ">" | "<"; +}; + +/** Verbal or simple constants evaluate to number */ +export type Constant = { + type: "constant"; + value: number; +}; + +/** Name of a character */ +export type Character = { + type: "character"; + name: string; +}; + +/** Reference to a character with pronoun/reflexive */ +export type CharacterRef = { + type: "characterRef"; + ref: "first" | "second"; +}; + +/** Leaf of an expression tree */ +export type AtomicExpression = Constant | Character | CharacterRef; + +/** Expression with binary operator */ +export type BinaryExpression = { + type: "binary"; + opType: "+" | "-" | "*" | "/" | "%"; + lhs: Expression; + rhs: Expression; +}; + +/** Expression with unary operator */ +export type UnaryExpression = { + type: "unary"; + opType: "!" | "sq" | "cube" | "sqrt" | "twice"; + operand: Expression; +}; + +/** Root of an expression tree or subtree */ +export type Expression = AtomicExpression | BinaryExpression | UnaryExpression; diff --git a/languages/shakespeare/parser/visitor.ts b/languages/shakespeare/parser/visitor.ts new file mode 100644 index 0000000..20f483a --- /dev/null +++ b/languages/shakespeare/parser/visitor.ts @@ -0,0 +1,617 @@ +import { CstNode, CstNodeLocation } from "chevrotain"; +import * as C from "./cst"; +import * as V from "./visitor-types"; +import { ShakespeareParser } from "./parser"; +import { ParseError } from "../../worker-errors"; +import { DocumentRange } from "../../types"; + +const parserInstance = new ShakespeareParser(); +const BaseShakespeareVisitor = parserInstance.getBaseCstVisitorConstructor(); + +export class ShakespeareVisitor + extends BaseShakespeareVisitor + implements V.ShakespeareVisitorTypes +{ + /** + * Characters of the program currently being visited. This field is populated + * in the `program` method, and then read by rule visitors for validating + * character names used in the program. + */ + private _characters: V.Character[] = []; + + constructor() { + super(); + // Visitor validation throws error for utility methods. There + // doesn't seem to be another way to allow private methods. + // this.validateVisitor(); + } + + /** + * Type-safe wrapper around Chevrotain's `visit` function. + * @param node CST node to visit + * @returns Visit result of node + */ + private visitNode( + node: T + // @ts-ignore TS complains, but does the job anyway + ): ReturnType { + return this.visit(node); + } + + /** + * Convert character name in source code to uniform character name + * @param image Character name used in source code + */ + private toCharacterId(image: string): string { + return image // "the \n Ghost" ... + .split(/[\s\n]+/) // ... -> ["the", "Ghost"] + .map((s) => s[0].toUpperCase() + s.slice(1)) // ... -> ["The", "Ghost"] + .join(" "); // ... -> "The Ghost" + } + + /** + * Convert a Chevrotain location object to a DocumentRange. Note + * that this assumes that the Chevrotain location object is + * fully populated with no undefined fields. + */ + private toRange(cstLocation: CstNodeLocation): DocumentRange { + const startLine = (cstLocation.startLine || 1) - 1; + const endLine = cstLocation.endLine && cstLocation.endLine - 1; + const startCol = cstLocation.startColumn && cstLocation.startColumn - 1; + const endCol = cstLocation.endColumn; // Chevrotain `endColumn` is inclusive + return { startLine, endLine, startCol, endCol }; + } + + /** + * Creates DocumentRange representing the range from the starting token to the + * ending token, both inclusive. + * @param start Range of the starting token + * @param end Range of the ending token + */ + private joinAndGetRange( + start: DocumentRange, + end: DocumentRange + ): DocumentRange { + return { + startLine: start.startLine, + startCol: start.startCol, + endLine: end.endLine, + endCol: end.endCol, + }; + } + + /** + * Check if usage of a character name is valid, ie. if the name is + * declared at the top of the program. + * @param character Character to check + * @param range Location of the character usage in source code + */ + private validateCharacter( + character: V.Character, + range: DocumentRange + ): void { + const charId = this.toCharacterId(character.name); + if (!this._characters.find((c) => c.name === charId)) { + throw new ParseError( + `Character '${character.name}' is not declared`, + range + ); + } + } + + program(children: C.ProgramCstChildren): V.Program { + if (children.characterIntro == null) children.characterIntro = []; + if (children.actSection == null) children.actSection = []; + const chars = children.characterIntro.map((c) => this.visitNode(c)); + this._characters = chars; // this must run before rest of program is visited + const acts = children.actSection.map((a) => this.visitNode(a)); + return { characters: chars, acts }; + } + + programTitle(): null { + throw new Error("Method not implemented."); + } + + characterIntro(children: C.CharacterIntroCstChildren): V.CharacterIntro { + const charId = this.toCharacterId(children.Character[0].image); + return { type: "character", name: charId }; + } + + actHeading(children: C.ActHeadingCstChildren): V.ActHeading { + return children.RomanNumeral[0].image.toUpperCase(); + } + + sceneHeading(children: C.SceneHeadingCstChildren): V.SceneHeading { + return children.RomanNumeral[0].image.toUpperCase(); + } + + entrance(children: C.EntranceCstChildren): V.EntryExitClause { + const chars: V.Character[] = children.Character.map((c) => ({ + type: "character", + name: this.toCharacterId(c.image), + })); + const range = this.joinAndGetRange( + this.toRange(children.SquareBracketOpen[0]), + this.toRange(children.SquareBracketClose[0]) + ); + chars.forEach((c) => this.validateCharacter(c, range)); + return { type: "entry", characters: chars, range }; + } + + exit(children: C.ExitCstChildren): V.EntryExitClause { + const charId = this.toCharacterId(children.Character[0].image); + const range = this.joinAndGetRange( + this.toRange(children.SquareBracketOpen[0]), + this.toRange(children.SquareBracketClose[0]) + ); + this.validateCharacter({ type: "character", name: charId }, range); + return { + type: "exit", + characters: [{ type: "character", name: charId }], + range, + }; + } + + multiExit(children: C.MultiExitCstChildren): V.EntryExitClause { + const range = this.joinAndGetRange( + this.toRange(children.SquareBracketOpen[0]), + this.toRange(children.SquareBracketClose[0]) + ); + + // `[Exeunt]`: all characters exit + if (children.Character == null) + return { type: "exit", characters: null, range }; + + const chars: V.Character[] = children.Character.map((c) => ({ + type: "character", + name: this.toCharacterId(c.image), + })); + chars.forEach((c) => this.validateCharacter(c, range)); + return { type: "exit", characters: chars, range }; + } + + entryExitClause(children: C.EntryExitClauseCstChildren): V.EntryExitClause { + if (children.entrance) return this.visitNode(children.entrance[0]); + else if (children.exit) return this.visitNode(children.exit[0]); + else if (children.multiExit) return this.visitNode(children.multiExit[0]); + else throw new Error("No matched subrule."); + } + + actSection(children: C.ActSectionCstChildren): V.ActSection { + if (children.sceneSection == null) children.sceneSection = []; + const id = this.visitNode(children.actHeading[0]); + const scenes = children.sceneSection.map((item) => this.visitNode(item)); + return { id, scenes }; + } + + sceneSection(children: C.SceneSectionCstChildren): V.SceneSection { + if (children.sceneSectionChunk == null) children.sceneSectionChunk = []; + const id = this.visitNode(children.sceneHeading[0]); + const items = children.sceneSectionChunk.flatMap( + (item) => { + const itemAst = this.visitNode(item); + if (itemAst.type !== "dialogue-set") return itemAst; + // Flatten the dialogue set into list of dialogue items + return itemAst.lines.map((line) => ({ + type: "dialogue-item", + speaker: itemAst.speaker, + line, + })); + } + ); + return { id, items }; + } + + sceneSectionChunk( + children: C.SceneSectionChunkCstChildren + ): V.SceneSectionChunk { + if (children.dialogueSet) return this.visitNode(children.dialogueSet[0]); + else if (children.entryExitClause) + return this.visitNode(children.entryExitClause[0]); + else throw new Error("No matched subrule."); + } + + speakerClause(children: C.SpeakerClauseCstChildren): V.SpeakerClause { + const charId = this.toCharacterId(children.Character[0].image); + this.validateCharacter( + { type: "character", name: charId }, + this.toRange(children.Character[0]) + ); + return { type: "character", name: charId }; + } + + dialogueSet(children: C.DialogueSetCstChildren): V.DialogueSet { + if (children.dialogueLine == null) children.dialogueLine = []; + const speaker = this.visitNode(children.speakerClause[0]); + const lines = children.dialogueLine.map((line) => this.visitNode(line)); + return { type: "dialogue-set", lines, speaker }; + } + + dialogueLine(children: C.DialogueLineCstChildren): V.DialogueLine { + if (children.conditional) return this.visitNode(children.conditional[0]); + else if (children.nonConditionalDialogueLine) + return this.visitNode(children.nonConditionalDialogueLine[0]); + else throw new Error("No matched subrule."); + } + + nonConditionalDialogueLine( + children: C.NonConditionalDialogueLineCstChildren + ): V.NonConditionalDialogueLine { + if (children.assignment) return this.visitNode(children.assignment[0]); + else if (children.stdin) return this.visitNode(children.stdin[0]); + else if (children.stdout) return this.visitNode(children.stdout[0]); + else if (children.goto) return this.visitNode(children.goto[0]); + else if (children.stackPush) return this.visitNode(children.stackPush[0]); + else if (children.stackPop) return this.visitNode(children.stackPop[0]); + else if (children.question) return this.visitNode(children.question[0]); + else throw new Error("No matched subrule."); + } + + /////////////////////////////// + ////////// ENGLISH ////////// + /////////////////////////////// + + noun(children: C.NounCstChildren) { + if (children.NegativeNoun) return -1 as const; + else if (children.NeutralNoun) return 0 as const; + else if (children.PositiveNoun) return 1 as const; + throw new Error("Invalid token found"); + } + + adjective(children: C.AdjectiveCstChildren) { + if (children.NegativeAdjective) return -1 as const; + else if (children.NeutralAdjective) return 0 as const; + else if (children.PositiveAdjective) return 1 as const; + throw new Error("Invalid token found"); + } + + possessive(children: C.PossessiveCstChildren) { + if (children.FirstPersonPossessive) return "first" as const; + else if (children.SecondPersonPossessive) return "second" as const; + else if (children.ThirdPersonPossessive) return "third" as const; + throw new Error("Invalid token found"); + } + + reflexive(children: C.ReflexiveCstChildren) { + if (children.FirstPersonReflexive) return "first" as const; + else if (children.SecondPersonReflexive) return "second" as const; + throw new Error("Invalid token found"); + } + + comparative(children: C.ComparativeCstChildren) { + if (children.NegativeComparative) return -1 as const; + else if (children.PositiveComparative) return 1 as const; + throw new Error("Invalid token found"); + } + + /////////////////////////////// + ///////// STATEMENTS //////// + /////////////////////////////// + + assignment(children: C.AssignmentCstChildren): V.AssignmentLine { + if (children.exclaimAssignment != null) + return this.visitNode(children.exclaimAssignment[0]); + else if (children.arithAssignment != null) + return this.visitNode(children.arithAssignment[0]); + else throw new Error("No matched subrule"); + } + + exclaimAssignment( + children: C.ExclaimAssignmentCstChildren + ): V.AssignmentLine { + const startRange = this.toRange(children.SecondPerson[0]); + const endRange = this.toRange(children.SentenceMark[0]); + return { + type: "assign", + value: this.visitNode(children.unarticulatedVerbalConstant[0]), + range: this.joinAndGetRange(startRange, endRange), + }; + } + + arithAssignment(children: C.ArithAssignmentCstChildren): V.AssignmentLine { + const startRange = this.toRange(children.SecondPerson[0]); + const endRange = this.toRange(children.SentenceMark[0]); + return { + type: "assign", + value: this.visitNode(children.expression[0]), + range: this.joinAndGetRange(startRange, endRange), + }; + } + + stdin(children: C.StdinCstChildren): V.StdinLine { + const startToken = + children.Open != null ? children.Open[0] : children.Listen![0]; + const endRange = this.toRange(children.SentenceMark[0]); + return { + type: "stdin", + inType: children.Heart == null ? "char" : "num", + range: this.joinAndGetRange(this.toRange(startToken), endRange), + }; + } + + stdout(children: C.StdoutCstChildren): V.StdoutLine { + const startToken = + children.Open != null ? children.Open[0] : children.Speak![0]; + const endRange = this.toRange(children.SentenceMark[0]); + return { + type: "stdout", + outType: children.Heart == null ? "char" : "num", + range: this.joinAndGetRange(this.toRange(startToken), endRange), + }; + } + + goto(children: C.GotoCstChildren): V.GotoLine { + const startToken = children.Let != null ? children.Let[0] : children.We![0]; + const endRange = this.toRange(children.SentenceMark[0]); + return { + type: "goto", + targetType: children.Act == null ? "scene" : "act", + target: children.RomanNumeral[0].image.toUpperCase(), + range: this.joinAndGetRange(this.toRange(startToken), endRange), + }; + } + + stackPush(children: C.StackPushCstChildren): V.StackPushLine { + return { + type: "stack-push", + expr: this.visitNode(children.expression[0]), + range: this.joinAndGetRange( + this.toRange(children.Remember[0]), + this.toRange(children.SentenceMark[0]) + ), + }; + } + + stackPop(children: C.StackPopCstChildren): V.StackPopLine { + return { + type: "stack-pop", + range: this.joinAndGetRange( + this.toRange(children.Recall[0]), + this.toRange(children.SentenceMark[0]) + ), + }; + } + + question(children: C.QuestionCstChildren): V.QuestionLine { + const comparator = this.visitNode(children.comparator[0]); + const lhs = this.visitNode(children.lhs[0]); + const rhs = this.visitNode(children.rhs[0]); + const range = this.joinAndGetRange( + this.toRange(children.Be[0]), + this.toRange(children.QuestionMark[0]) + ); + return { type: "question", comparator, lhs, rhs, range }; + } + + conditional(children: C.ConditionalCstChildren): V.ConditionalLine { + const consequent = this.visitNode(children.nonConditionalDialogueLine[0]); + const invert = children.Not != null; + const startRange = this.toRange(children.If[0]); + const range = this.joinAndGetRange(startRange, consequent.range); + return { type: "conditional", consequent, invert, range }; + } + + comparator(children: C.ComparatorCstChildren): V.ComparisonOperator { + const invert = children.Not != null; + let comparator: V.ComparisonOperator = { type: "==" }; + if (children._asComparator) { + comparator = this.visitNode(children._asComparator[0]); + } else if (children._simpleComparator) { + comparator = this.visitNode(children._simpleComparator[0]); + } else if (children._moreLessComparator) { + comparator = this.visitNode(children._moreLessComparator[0]); + } else throw new Error("No matched subrule."); + comparator.invert = invert; + return comparator; + } + + _asComparator(): V.ComparisonOperator { + return { type: "==" }; + } + + _simpleComparator( + children: C._simpleComparatorCstChildren + ): V.ComparisonOperator { + const comperative = this.visitNode(children.comparative[0]); + return { type: comperative === -1 ? "<" : ">" }; + } + + _moreLessComparator( + children: C._moreLessComparatorCstChildren + ): V.ComparisonOperator { + // "<": more OR less + // ">": more OR less + const adjValue = this.visitNode(children.adjective[0]); + if (adjValue === 0) + throw new ParseError( + "Cannot use neutral adjective as a comparator", + this.toRange(children.adjective[0].location!) + ); + + if (children.More) return { type: adjValue === -1 ? "<" : ">" }; + else if (children.Less) return { type: adjValue === -1 ? ">" : "<" }; + else throw new Error("Unexpected missing token"); + } + + /////////////////////////////// + ///////// CONSTANTS ///////// + /////////////////////////////// + + constant(children: C.ConstantCstChildren): V.Constant { + if (children._simpleConstant != null) + return this.visitNode(children._simpleConstant[0]); + else if (children._verbalConstant != null) + return this.visitNode(children._verbalConstant[0]); + throw new Error("Unexpected missing subrule"); + } + + _simpleConstant(): V.Constant { + return { type: "constant", value: 0 }; + } + + _verbalConstant(children: C._verbalConstantCstChildren): V.Constant { + return this.visitNode(children.unarticulatedVerbalConstant[0]); + } + + unarticulatedVerbalConstant( + children: C.UnarticulatedVerbalConstantCstChildren + ) { + if (children.adjective == null) children.adjective = []; + if (children.noun == null) throw new Error("Missing noun token"); + + let nounValue = this.visitNode(children.noun[0]); + if (nounValue === -1) { + // Negative noun: all adjectives must be neutral or negative + for (let i = 0; i < children.adjective.length; i++) { + let adjectiveValue = this.visitNode(children.adjective[i]); + if (adjectiveValue !== 0 && adjectiveValue !== -1) + throw new ParseError( + "Negative noun only allows negative adjectives", + this.toRange(children.adjective[i].location!) + ); + } + const value = -1 * 2 ** children.adjective.length; + return { type: "constant" as const, value }; + } else { + // Positive noun: all adjectives must be neutral or positive + for (let i = 0; i < children.adjective.length; i++) { + let adjectiveValue = this.visitNode(children.adjective[i]); + if (adjectiveValue !== 0 && adjectiveValue !== 1) + throw new ParseError( + "Positive noun only allows positive or neutral adjectives", + this.toRange(children.adjective[i].location!) + ); + } + const value = 1 * 2 ** children.adjective.length; + return { type: "constant" as const, value }; + } + } + + /////////////////////////////// + ////// EXPRESSION TREE ////// + /////////////////////////////// + + expression(children: C.ExpressionCstChildren): V.Expression { + if (children.atomicExpression != null) + return this.visitNode(children.atomicExpression[0]); + else if (children.sumExpression != null) + return this.visitNode(children.sumExpression[0]); + else if (children.differenceExpression != null) + return this.visitNode(children.differenceExpression[0]); + else if (children.productExpression != null) + return this.visitNode(children.productExpression[0]); + else if (children.quotientExpression != null) + return this.visitNode(children.quotientExpression[0]); + else if (children.remainderExpression != null) + return this.visitNode(children.remainderExpression[0]); + else if (children.factorialExpression != null) + return this.visitNode(children.factorialExpression[0]); + else if (children.squareExpression != null) + return this.visitNode(children.squareExpression[0]); + else if (children.cubeExpression != null) + return this.visitNode(children.cubeExpression[0]); + else if (children.squareRootExpression != null) + return this.visitNode(children.squareRootExpression[0]); + else if (children.twiceExpression != null) + return this.visitNode(children.twiceExpression[0]); + else throw new Error("No matched subrule"); + } + + atomicExpression( + children: C.AtomicExpressionCstChildren + ): V.AtomicExpression { + if (children.Character != null) { + const charId = this.toCharacterId(children.Character[0].image); + this.validateCharacter( + { type: "character", name: charId }, + this.toRange(children.Character[0]) + ); + return { type: "character" as const, name: charId }; + } else if (children.FirstPerson != null) { + return { type: "characterRef", ref: "first" }; + } else if (children.SecondPerson != null) { + return { type: "characterRef", ref: "second" }; + } else if (children.reflexive != null) { + const ref = this.visitNode(children.reflexive[0]); + return { type: "characterRef", ref }; + } else if (children.constant != null) { + return this.visitNode(children.constant[0]); + } else throw new Error("No matched subrule"); + } + + private visitBinaryExpression( + code: V.BinaryExpression["opType"], + lhs: C.ExpressionCstNode[], + rhs: C.ExpressionCstNode[] + ): V.BinaryExpression { + if (lhs.length !== 1 || rhs.length !== 1) + throw new Error("Unexpected operands in binary expression"); + return { + type: "binary", + opType: code, + lhs: this.visitNode(lhs[0]), + rhs: this.visitNode(rhs[0]), + }; + } + + private visitUnaryExpression( + code: V.UnaryExpression["opType"], + expr: C.ExpressionCstNode[] + ): V.UnaryExpression { + if (expr.length !== 1) + throw new Error("Unexpected operands in unary expression"); + return { type: "unary", opType: code, operand: this.visitNode(expr[0]) }; + } + + sumExpression(children: C.SumExpressionCstChildren): V.BinaryExpression { + return this.visitBinaryExpression("+", children.lhs, children.rhs); + } + + differenceExpression( + children: C.DifferenceExpressionCstChildren + ): V.BinaryExpression { + return this.visitBinaryExpression("-", children.lhs, children.rhs); + } + + productExpression( + children: C.ProductExpressionCstChildren + ): V.BinaryExpression { + return this.visitBinaryExpression("*", children.lhs, children.rhs); + } + + quotientExpression( + children: C.QuotientExpressionCstChildren + ): V.BinaryExpression { + return this.visitBinaryExpression("/", children.lhs, children.rhs); + } + + remainderExpression( + children: C.RemainderExpressionCstChildren + ): V.BinaryExpression { + return this.visitBinaryExpression("%", children.lhs, children.rhs); + } + + factorialExpression( + children: C.FactorialExpressionCstChildren + ): V.UnaryExpression { + return this.visitUnaryExpression("!", children.expression); + } + + squareExpression(children: C.SquareExpressionCstChildren): V.UnaryExpression { + return this.visitUnaryExpression("sq", children.expression); + } + + cubeExpression(children: C.CubeExpressionCstChildren): V.UnaryExpression { + return this.visitUnaryExpression("cube", children.expression); + } + + squareRootExpression( + children: C.SquareRootExpressionCstChildren + ): V.UnaryExpression { + return this.visitUnaryExpression("sqrt", children.expression); + } + + twiceExpression(children: C.TwiceExpressionCstChildren): V.UnaryExpression { + return this.visitUnaryExpression("twice", children.expression); + } +} diff --git a/languages/shakespeare/renderer/character-row.tsx b/languages/shakespeare/renderer/character-row.tsx new file mode 100644 index 0000000..741e34a --- /dev/null +++ b/languages/shakespeare/renderer/character-row.tsx @@ -0,0 +1,24 @@ +import { CharacterValue } from "../common"; +import { SimpleTag } from "./utils"; + +type Props = { + name: string; + value: CharacterValue; +}; + +export const CharacterRow = (props: Props) => { + const { name, value } = props; + + return ( +
+
+ {name}:{" "} +
{value.value}
+ {value.stack.map((v, i) => ( + {v} + ))} +
+
+
+ ); +}; diff --git a/languages/shakespeare/renderer/index.tsx b/languages/shakespeare/renderer/index.tsx new file mode 100644 index 0000000..aa083d2 --- /dev/null +++ b/languages/shakespeare/renderer/index.tsx @@ -0,0 +1,63 @@ +import { Colors } from "@blueprintjs/core"; +import { RendererProps } from "../../types"; +import { RS } from "../common"; +import { CharacterRow } from "./character-row"; +import { TopBar } from "./topbar"; + +/** Common border color for dark and light, using transparency */ +export const BorderColor = Colors.GRAY3 + "55"; + +const styles = { + placeholderDiv: { + height: "100%", + width: "100%", + display: "flex", + justifyContent: "center", + alignItems: "center", + fontSize: "1.2em", + }, + rootContainer: { + height: "100%", + display: "flex", + flexDirection: "column" as "column", + }, + topBarContainer: { + borderBottom: "1px solid " + BorderColor, + padding: 10, + }, + mainContainer: { + flex: 1, + minHeight: 0, + overflowY: "auto" as "auto", + }, +}; + +export const Renderer = ({ state }: RendererProps) => { + if (state == null) + return ( +
Run some code to see the stage!
+ ); + + return ( +
+
+ +
+
+ {Object.keys(state.characterBag).map((name) => ( + + ))} +
+
+ ); + + return null; +}; diff --git a/languages/shakespeare/renderer/topbar.tsx b/languages/shakespeare/renderer/topbar.tsx new file mode 100644 index 0000000..07c7533 --- /dev/null +++ b/languages/shakespeare/renderer/topbar.tsx @@ -0,0 +1,56 @@ +import { Tag, Text } from "@blueprintjs/core"; +import { SimpleTag } from "./utils"; + +const styles = { + charChip: { + margin: "0 5px", + }, + questionText: { + marginLeft: 30, + marginRight: 10, + }, +}; + +type Props = { + charactersOnStage: string[]; + currSpeaker: string | null; + questionState: boolean | null; +}; + +export const TopBar = (props: Props) => { + const { charactersOnStage, currSpeaker, questionState } = props; + + const characterChips = + charactersOnStage.length === 0 ? ( + + The stage is empty + + ) : ( + charactersOnStage.map((character) => { + return ( + + {character} + + ); + }) + ); + + return ( +
+ {characterChips} + {questionState != null && ( + <> + + Answer to question: + + + {questionState ? "yes" : "no"} + + + )} +
+ ); +}; diff --git a/languages/shakespeare/renderer/utils.tsx b/languages/shakespeare/renderer/utils.tsx new file mode 100644 index 0000000..a1f901d --- /dev/null +++ b/languages/shakespeare/renderer/utils.tsx @@ -0,0 +1,60 @@ +import { Colors } from "@blueprintjs/core"; +import { useDarkMode } from "../../../ui/providers/dark-mode-provider"; + +const backgroundColorsLight = { + success: Colors.GREEN3, + danger: Colors.RED3, + plain: Colors.GRAY3, + active: Colors.DARK_GRAY1, +}; + +const backgroundColorsDark = { + success: Colors.GREEN3, + danger: Colors.RED3, + plain: Colors.GRAY3, + active: Colors.LIGHT_GRAY5, +}; + +const foregroundColorsLight = { + success: Colors.GREEN2, + danger: Colors.RED2, + plain: Colors.DARK_GRAY1, + active: Colors.LIGHT_GRAY5, +}; + +const foregroundColorsDark = { + success: Colors.GREEN5, + danger: Colors.RED5, + plain: Colors.LIGHT_GRAY5, + active: Colors.DARK_GRAY1, +}; + +/** + * Utility component that renders a tag similar to BlueprintJS tags, but underneath + * is just a single span tag with no frills and high performance. + */ +export const SimpleTag = (props: { + children: React.ReactNode; + intent?: "success" | "danger" | "active"; +}) => { + const { isDark } = useDarkMode(); + const intent = props.intent == null ? "plain" : props.intent; + const backgroundMap = isDark ? backgroundColorsDark : backgroundColorsLight; + const foregroundMap = isDark ? foregroundColorsDark : foregroundColorsLight; + + return ( + + {props.children} + + ); +}; diff --git a/languages/shakespeare/runtime.ts b/languages/shakespeare/runtime.ts new file mode 100644 index 0000000..e40e788 --- /dev/null +++ b/languages/shakespeare/runtime.ts @@ -0,0 +1,355 @@ +import { DocumentRange, LanguageEngine, StepExecutionResult } from "../types"; +import { RuntimeError } from "../worker-errors"; +import { CharacterBag, RS } from "./common"; +import InputStream from "./input-stream"; +import { Parser } from "./parser"; +import * as V from "./parser/visitor-types"; + +/** Runtime program counter */ +type PC = { + act: number; // Current act, indexed in parsing order + scene: number; // Current scene, indexed in parsing order in the current act + itemIdx: number; // Current item, indexed in parsing order in the current scene +}; + +/** List of character currently on stage */ +type Stage = string[]; + +const DEFAULT_QN_RESULT: boolean | null = null; +const DEFAULT_SPEAKER: string | null = null; +const DEFAULT_STAGE = (): Stage => []; +const DEFAULT_AST = (): V.Program => ({ characters: [], acts: [] }); +const DEFAULT_CHARBAG = (): CharacterBag => ({}); +const DEFAULT_PC = (): PC => ({ act: 0, scene: 0, itemIdx: -1 }); + +export default class ShakespeareLanguageEngine implements LanguageEngine { + private _parser: Parser = new Parser(); + private _charBag: CharacterBag = DEFAULT_CHARBAG(); + private _ast: V.Program = DEFAULT_AST(); + private _pc: PC = DEFAULT_PC(); + private _stage: Stage = DEFAULT_STAGE(); + private _currSpeaker: string | null = DEFAULT_SPEAKER; + private _qnResult: boolean | null = DEFAULT_QN_RESULT; + private _input: InputStream = new InputStream(""); + + resetState() { + this._charBag = DEFAULT_CHARBAG(); + this._ast = DEFAULT_AST(); + this._pc = DEFAULT_PC(); + this._stage = DEFAULT_STAGE(); + this._currSpeaker = DEFAULT_SPEAKER; + this._qnResult = DEFAULT_QN_RESULT; + this._input = new InputStream(""); + } + + validateCode(code: string) { + this._parser.parse(code); + } + + prepare(code: string, input: string) { + this._ast = this._parser.parse(code); + this._input = new InputStream(input); + // Populate the character bag + for (const character of this._ast.characters) + this._charBag[character.name] = { value: 0, stack: [] }; + // Set the PC to first act, first scene, first item + this._pc = { act: 0, scene: 0, itemIdx: 0 }; + } + + executeStep(): StepExecutionResult { + let output: string | undefined = undefined; + let finished: boolean = false; + + // Execute the next step + if (this._pc.itemIdx !== -1) { + const item = this.getCurrentItem(); + output = this.processSceneItem(item); + finished = this.validateAndWrapPC(); + } else { + this.setCharacterBag(); + this._pc.itemIdx += 1; + } + + // Set the next value of current speaker and prepare + // location of next step + let nextStepLocation = null; + if (!finished) { + const item = this.getCurrentItem(); + nextStepLocation = this.getItemRange(item); + if (item.type !== "dialogue-item") this._currSpeaker = null; + else this._currSpeaker = item.speaker.name; + } else this._currSpeaker = null; + + // Prepare renderer state, and return + const rendererState: RS = { + characterBag: this._charBag, + charactersOnStage: this._stage, + currentSpeaker: this._currSpeaker, + questionState: this._qnResult, + }; + return { rendererState, nextStepLocation, output }; + } + + /** Get the DocumentRange of a scene item */ + private getItemRange(item: V.SceneSectionItem): DocumentRange { + if (item.type === "dialogue-item") return item.line.range; + else return item.range; + } + + /** Create and set the character bag from the character intros in AST */ + private setCharacterBag() { + for (const character of this._ast.characters) + this._charBag[character.name] = { value: 0, stack: [] }; + } + + /** + * Get item pointed to by current PC. Ensure that current PC + * points to a valid scene item. + */ + private getCurrentItem(): V.SceneSectionItem { + const currAct = this._ast.acts[this._pc.act]; + const currScene = currAct.scenes[this._pc.scene]; + return currScene.items[this._pc.itemIdx]; + } + + /** Process a single item of a scene */ + private processSceneItem(item: V.SceneSectionItem): string | undefined { + if (item.type === "entry" || item.type === "exit") + this.processEntryExit(item); + else if (item.type === "dialogue-item") { + return this.processDialogueLine(item.line); + } else throw new Error("Unknown scene item type"); + } + + /** Process a single dialogue line */ + private processDialogueLine(line: V.DialogueLine): string | undefined { + if (line.type === "conditional") return this.processConditional(line); + this._qnResult = null; // Clear question result + if (line.type === "assign") this.processAssignment(line); + else if (line.type === "stdin") this.processStdinLine(line); + else if (line.type === "stdout") return this.processStdoutLine(line); + else if (line.type === "goto") this.processGotoLine(line); + else if (line.type === "stack-push") this.processStackPush(line); + else if (line.type === "stack-pop") this.processStackPop(line); + else if (line.type === "question") this.processQuestion(line); + else throw new Error("Unknown dialogue type"); + } + + /** Process assignment dialogue line */ + private processAssignment(line: V.AssignmentLine): void { + this.incrementPC(); + const other = this.dereference("second"); + const value = this.evaluateExpression(line.value); + this._charBag[other].value = value; + } + + /** Process STDIN dialogue line */ + private processStdinLine(line: V.StdinLine): void { + this.incrementPC(); + let value = 0; + if (line.inType === "num") value = this._input.getNumber(); + else value = this._input.getChar(); + const other = this.dereference("second"); + this._charBag[other].value = value; + } + + /** Process STDOUT dialogue line */ + private processStdoutLine(line: V.StdoutLine): string { + this.incrementPC(); + const other = this.dereference("second"); + const value = this._charBag[other].value; + if (line.outType === "num") return value.toString(); + else return String.fromCharCode(value); + } + + /** Process goto dialogue line and update program counter */ + private processGotoLine(line: V.GotoLine): void { + if (line.targetType === "act") { + // ======= JUMP TO ACT ======== + const actIdx = this._ast.acts.findIndex((act) => act.id === line.target); + if (actIdx === -1) throw new RuntimeError(`Unknown act '${line.target}'`); + this._pc = { act: actIdx, scene: 0, itemIdx: 0 }; + } else { + // ======= JUMP TO SCENE ======== + const actIdx = this._pc.act; + const sceneIdx = this._ast.acts[actIdx].scenes.findIndex( + (scene) => scene.id === line.target + ); + if (sceneIdx === -1) + throw new RuntimeError(`Unknown scene '${line.target}'`); + this._pc = { act: actIdx, scene: sceneIdx, itemIdx: 0 }; + } + } + + /** Process stack push dialogue line */ + private processStackPush(line: V.StackPushLine): void { + this.incrementPC(); + const other = this.dereference("second"); + const value = this.evaluateExpression(line.expr); + this._charBag[other].stack.push(value); + } + + /** Process stack pop dialogue line */ + private processStackPop(_line: V.StackPopLine): void { + this.incrementPC(); + const other = this.dereference("second"); + const value = this._charBag[other].stack.pop(); + if (value == null) + throw new RuntimeError(`Character '${other}' has empty stack`); + this._charBag[other].value = value; + } + + /** Process question dialogue line */ + private processQuestion(line: V.QuestionLine): void { + this.incrementPC(); + const lhsValue = this.evaluateExpression(line.lhs); + const rhsValue = this.evaluateExpression(line.rhs); + let answer = true; + const op = line.comparator.type; + if (op === "==") answer = lhsValue === rhsValue; + else if (op === "<") answer = lhsValue < rhsValue; + else if (op === ">") answer = lhsValue > rhsValue; + if (line.comparator.invert) answer = !answer; + this._qnResult = answer; + } + + /** Process conditional dialogue line */ + private processConditional(line: V.ConditionalLine): string | undefined { + if (this._qnResult == null) + throw new RuntimeError("Question not asked before conditional"); + const answer = line.invert ? !this._qnResult : this._qnResult; + this._qnResult = null; // Clear question result + if (answer) return this.processDialogueLine(line.consequent); + else this.incrementPC(); + } + + /** Add or remove characters from the stage as per the clause */ + private processEntryExit(clause: V.EntryExitClause): void { + this.incrementPC(); + const { characters, type } = clause; + if (type === "entry") { + // ========= ENTRY CLAUSE ========= + for (const char of characters!) { + if (this._stage.includes(char.name)) { + // Entry of character already on stage + throw new RuntimeError(`Character '${char.name}' already on stage`); + } else if (this._stage.length === 2) { + // Stage is full capacity + throw new RuntimeError("Too many characters on stage"); + } else this._stage.push(char.name); + } + } else { + // ========= EXIT CLAUSE ========= + if (characters != null) { + for (const char of characters) { + if (!this._stage.includes(char.name)) { + // Exit of character not on stage + throw new RuntimeError(`Character '${char.name}' is not on stage`); + } else this._stage.splice(this._stage.indexOf(char.name), 1); + } + } else { + // Exit of all characters + this._stage = []; + } + } + } + + /** + * Increment program counter. Does not wrap to next act or scene - + * that is handled in the `executeStep` method. + */ + private incrementPC(): void { + this._pc.itemIdx += 1; + } + + /** + * Check that the PC is in scene bounds. If not, + * wrap it over to the next scene or act. + * @returns True if program has ended, false otherwise + */ + private validateAndWrapPC(): boolean { + const { act, scene, itemIdx } = this._pc; + const currentScene = this._ast.acts[act].scenes[scene]; + if (itemIdx >= currentScene.items.length) { + if (scene === this._ast.acts[act].scenes.length - 1) { + // Check if we're at the end of the program + if (act === this._ast.acts.length - 1) return true; + // Wrap to the next act + this._pc.act += 1; + this._pc.scene = 0; + this._pc.itemIdx = 0; + } else { + // Wrap to the next scene + this._pc.scene += 1; + this._pc.itemIdx = 0; + } + } + return false; + } + + /** Dereference what character "you" or "me" refers to */ + private dereference(type: "first" | "second"): string { + if (type === "first") { + // Current speaker is the required character + if (!this._currSpeaker) throw new RuntimeError("No active speaker"); + else return this._currSpeaker; + } else { + // The other character is the required character + if (this._stage.length === 1) + throw new RuntimeError("Only one character on stage"); + return this._stage.find((char) => char !== this._currSpeaker)!; + } + } + + /** + * Evaluate the given expression and return the numeric result. + * @param expr Expression to evaluate + * @returns Numeric value of the expression + */ + private evaluateExpression(expr: V.Expression): number { + if (expr.type === "constant") return expr.value; + else if (expr.type === "character") { + // === NAMED REFERENCE TO CHARACTER === + const { name } = expr; + if (!this._charBag.hasOwnProperty(name)) + throw new RuntimeError(`Character '${name}' not found`); + return this._charBag[name].value; + } else if (expr.type === "characterRef") { + // === PRONOUN REFERENCE TO CHARACTER === + const name = this.dereference(expr.ref); + return this._charBag[name].value; + } else if (expr.type === "binary") { + // ======= BINARY EXPRESSION ======= + const { opType, lhs, rhs } = expr; + const lhsValue = this.evaluateExpression(lhs); + const rhsValue = this.evaluateExpression(rhs); + if (opType === "+") return lhsValue + rhsValue; + if (opType === "-") return lhsValue - rhsValue; + if (opType === "*") return lhsValue * rhsValue; + // For division and modulus, we need to check for division by zero + if (rhsValue === 0) throw new RuntimeError("Division by zero"); + if (opType === "/") return Math.floor(lhsValue / rhsValue); + if (opType === "%") return lhsValue % rhsValue; + throw new Error(`Unknown operator '${opType}'`); + } else if (expr.type === "unary") { + // ======== UNARY EXPRESSION ======== + const { opType, operand } = expr; + const operandValue = this.evaluateExpression(operand); + if (opType === "!") return this.factorial(operandValue); + if (opType === "sq") return operandValue ** 2; + if (opType === "cube") return operandValue ** 3; + if (opType === "sqrt") return Math.floor(Math.sqrt(operandValue)); + if (opType === "twice") return 2 * operandValue; + throw new Error(`Unknown operator '${opType}'`); + } else throw new Error(`Unknown expression type`); + } + + /** Compute the factorial of a number */ + private factorial(n: number): number { + if (n < 0) + throw new RuntimeError("Cannot compute factorial of negative number"); + let answer = 1; + for (let i = 1; i <= n; ++i) answer *= i; + return answer; + } +} diff --git a/languages/shakespeare/tests/helloworld.txt b/languages/shakespeare/tests/helloworld.txt new file mode 100644 index 0000000..cd55cbb --- /dev/null +++ b/languages/shakespeare/tests/helloworld.txt @@ -0,0 +1,89 @@ +The Infamous Hello World Program. + +Romeo, a young man with a remarkable patience. +Juliet, a likewise young woman of remarkable grace. +Ophelia, a remarkable woman much in dispute with Hamlet. +Hamlet, the flatterer of Andersen Insulting A/S. + + + Act I: Hamlet's insults and flattery. + + Scene I: The insulting of Romeo. + +[Enter Hamlet and Romeo] + +Hamlet: + You lying stupid fatherless big smelly half-witted coward! + You are as stupid as the difference between a handsome rich brave + hero and thyself! Speak your mind! + + You are as brave as the sum of your fat little stuffed misused dusty + old rotten codpiece and a beautiful fair warm peaceful sunny summer's + day. You are as healthy as the difference between the sum of the + sweetest reddest rose and my father and yourself! Speak your mind! + + You are as cowardly as the sum of yourself and the difference + between a big mighty proud kingdom and a horse. Speak your mind. + + Speak your mind! + +[Exit Romeo] + + Scene II: The praising of Juliet. + +[Enter Juliet] + +Hamlet: + Thou art as sweet as the sum of the sum of Romeo and his horse and his + black cat! Speak thy mind! + +[Exit Juliet] + + Scene III: The praising of Ophelia. + +[Enter Ophelia] + +Hamlet: + Thou art as lovely as the product of a large rural town and my amazing + bottomless embroidered purse. Speak thy mind! + + Thou art as loving as the product of the bluest clearest sweetest sky + and the sum of a squirrel and a white horse. Thou art as beautiful as + the difference between Juliet and thyself. Speak thy mind! + +[Exeunt Ophelia and Hamlet] + + + Act II: Behind Hamlet's back. + + Scene I: Romeo and Juliet's conversation. + +[Enter Romeo and Juliet] + +Romeo: + Speak your mind. You are as worried as the sum of yourself and the + difference between my small smooth hamster and my nose. Speak your + mind! + +Juliet: + Speak YOUR mind! You are as bad as Hamlet! You are as small as the + difference between the square of the difference between my little pony + and your big hairy hound and the cube of your sorry little + codpiece. Speak your mind! + +[Exit Romeo] + + Scene II: Juliet and Ophelia's conversation. + +[Enter Ophelia] + +Juliet: + Thou art as good as the quotient between Romeo and the sum of a small + furry animal and a leech. Speak your mind! + +Ophelia: + Thou art as disgusting as the quotient between Romeo and twice the + difference between a mistletoe and an oozing infected blister! Speak + your mind! + +[Exeunt] diff --git a/languages/shakespeare/tests/index.test.ts b/languages/shakespeare/tests/index.test.ts new file mode 100644 index 0000000..a939090 --- /dev/null +++ b/languages/shakespeare/tests/index.test.ts @@ -0,0 +1,24 @@ +import { executeProgram, readTestProgram } from "../../test-utils"; +import Engine from "../runtime"; + +describe("Test programs", () => { + test("Hello World", async () => { + const code = readTestProgram(__dirname, "helloworld"); + const result = await executeProgram(new Engine(), code); + expect(result.output).toBe("Hello World!\n"); + }); + + test("Prime Numbers", async () => { + const code = readTestProgram(__dirname, "primes"); + const result = await executeProgram(new Engine(), code, "15"); + expect(result.output).toBe(">2 3 5 7 11 13 "); + }); + + test("Reverse cat", async () => { + const code = readTestProgram(__dirname, "reverse"); + const input = "abcd efgh\nijkl mnop\n"; + const expectedOutput = input.split("").reverse().join(""); + const result = await executeProgram(new Engine(), code, input); + expect(result.output).toBe(expectedOutput); + }); +}); diff --git a/languages/shakespeare/tests/primes.txt b/languages/shakespeare/tests/primes.txt new file mode 100644 index 0000000..43bb9ab --- /dev/null +++ b/languages/shakespeare/tests/primes.txt @@ -0,0 +1,104 @@ +=== Modification in Act II, Scene III, Romeo's line === +=== Numbers separated by space instead of newline === +Prime Number Computation in Copenhagen. + +Romeo, a young man of Verona. +Juliet, a young woman. +Hamlet, a temporary variable from Denmark. +The Ghost, a limiting factor (and by a remarkable coincidence also Hamlet's father). + + + Act I: Interview with the other side. + + Scene I: At the last hour before dawn. + +[Enter the Ghost and Juliet] + +The Ghost: + You pretty little warm thing! Thou art as prompt as the difference + between the square of thyself and your golden hair. Speak your mind. + +Juliet: + Listen to your heart! + +[Exit the Ghost] + +[Enter Romeo] + +Juliet: + Thou art as sweet as a sunny summer's day! + + + Act II: Determining divisibility. + + Scene I: A private conversation. + +Juliet: + Art thou more cunning than the Ghost? + +Romeo: + If so, let us proceed to scene V. + +[Exit Romeo] + +[Enter Hamlet] + +Juliet: + You are as villainous as the square root of Romeo! + +Hamlet: + You are as lovely as a red rose. + + Scene II: Questions and the consequences thereof. + +Juliet: + Am I better than you? + +Hamlet: + If so, let us proceed to scene III. + +Juliet: + Is the remainder of the quotient between Romeo and me as good as + nothing? + +Hamlet: + If so, let us proceed to scene IV. + Thou art as bold as the sum of thyself and a roman. + +Juliet: + Let us return to scene II. + + Scene III: Romeo must die! + +[Exit Hamlet] + +[Enter Romeo] + +Juliet: + Open your heart. + +[Exit Juliet] + +[Enter Hamlet] + +Romeo: + Thou art as rotten as the difference between nothing and a vile + disgusting snotty stinking half-witted hog. Speak your mind! + +[Exit Romeo] + +[Enter Juliet] + + Scene IV: One small dog at a time. + +[Exit Hamlet] + +[Enter Romeo] + +Juliet: + Thou art as handsome as the sum of thyself and my chihuahua! + Let us return to scene I. + + Scene V: Fin. + +[Exeunt] diff --git a/languages/shakespeare/tests/reverse.txt b/languages/shakespeare/tests/reverse.txt new file mode 100644 index 0000000..5bde250 --- /dev/null +++ b/languages/shakespeare/tests/reverse.txt @@ -0,0 +1,45 @@ +Outputting Input Reversedly. + +Othello, a stacky man. +Lady Macbeth, who pushes him around till he pops. + + + Act I: The one and only. + + Scene I: In the beginning, there was nothing. + +[Enter Othello and Lady Macbeth] + +Othello: + You are nothing! + + Scene II: Pushing to the very end. + +Lady Macbeth: + Open your mind! Remember yourself. + +Othello: + You are as hard as the sum of yourself and a stone wall. Am I as + horrid as a flirt-gill? + +Lady Macbeth: + If not, let us return to scene II. Recall your imminent death! + +Othello: + You are as small as the difference between yourself and a hair! + + Scene III: Once you pop, you can't stop! + +Lady Macbeth: + Recall your unhappy childhood. Speak your mind! + +Othello: + You are as vile as the sum of yourself and a toad! Are you better + than nothing? + +Lady Macbeth: + If so, let us return to scene III. + + Scene IV: The end. + +[Exeunt] diff --git a/package.json b/package.json index afb5a35..2fe2ea9 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@blueprintjs/core": "^4.0.0-beta.12", "@monaco-editor/react": "^4.3.1", + "chevrotain": "^10.0.0", "monaco-editor": "^0.30.1", "next": "12.0.7", "react": "17.0.2", @@ -30,6 +31,7 @@ "jest": "^27.4.7", "peggy": "^1.2.0", "ts-loader": "^9.2.6", + "ts-node": "^10.5.0", "typescript": "4.5.2", "webpack": "^5.65.0", "webpack-cli": "^4.9.1", diff --git a/pages/ide/shakespeare.tsx b/pages/ide/shakespeare.tsx new file mode 100644 index 0000000..7265716 --- /dev/null +++ b/pages/ide/shakespeare.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import { NextPage } from "next"; +import Head from "next/head"; +import { Mainframe } from "../../ui/Mainframe"; +import LangProvider from "../../languages/shakespeare"; +const LANG_ID = "shakespeare"; +const LANG_NAME = "Shakespeare"; + +const IDE: NextPage = () => { + return ( + <> + + {LANG_NAME} | Esolang Park + + + + ); +}; + +export default IDE; diff --git a/pages/languages.json b/pages/languages.json index e4d6e5e..ed0a0d5 100644 --- a/pages/languages.json +++ b/pages/languages.json @@ -14,5 +14,9 @@ { "display": "Deadfish", "id": "deadfish" + }, + { + "display": "Shakespeare", + "id": "shakespeare" } -] +] \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index a84a3a7..00fc5f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -387,6 +387,45 @@ classnames "^2.2" tslib "~1.13.0" +"@chevrotain/cst-dts-gen@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.0.0.tgz#2e3a35df732f2eb777bc8e0ca8e2ea211881f783" + integrity sha512-aiykxaxLAckRvINV8JtArhVtfQvbb2wr1inWdr+kPO7n6OJJgXVAVCLgb+UE1VPBlCWzPAn6JnV7BSIoZDJ3Fg== + dependencies: + "@chevrotain/gast" "^10.0.0" + "@chevrotain/types" "^10.0.0" + lodash "4.17.21" + +"@chevrotain/gast@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/gast/-/gast-10.0.0.tgz#9c6fe9902c79d0217e2ef06488b98b61ddd103c5" + integrity sha512-R9VT8/i9br7dKsFdeNsUn897CuA4UWyBqVkK60kPA9+TEtamPTPEEiRMPDoqBTMADejqUeGzdq4CXDmioFh7tA== + dependencies: + "@chevrotain/types" "^10.0.0" + lodash "4.17.21" + +"@chevrotain/types@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/types/-/types-10.0.0.tgz#b437da38b0dab358b4ded8cc2e36296b59bce73f" + integrity sha512-lSsFTZDX5jwXA1mTpwO92YrNZA+y7OIREZnv1qJwqOQ/bvFLVK0IX4r1Oma7OouE6cQQywxpT/+PlePGPGjoAw== + +"@chevrotain/utils@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@chevrotain/utils/-/utils-10.0.0.tgz#ccff6f9d69dadcaffe49df0bcd8f30be6a8a2415" + integrity sha512-SkqgApBnHCwXyRubC5/C3s8kBDs8naC7HwrW3keArIXAPxbtYMjJXG6xXr31D5a9MXeB7MFKtXEXKH0FqF0eqQ== + +"@cspotcode/source-map-consumer@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz#33bf4b7b39c178821606f669bbc447a6a629786b" + integrity sha512-41qniHzTU8yAGbCp04ohlmSrZf8bkf/iJsl3V0dRGsQN/5GFfx+LbCSsCpp2gqrqjTVg/K6O8ycoV35JIwAzAg== + +"@cspotcode/source-map-support@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.7.0.tgz#4789840aa859e46d2f3173727ab707c66bf344f5" + integrity sha512-X4xqRHqN8ACt2aHVe51OxeA2HjbcL4MqFqXkrmQszJ1NOUuUu5u6Vqx/0lZSVNku7velL5FC/s5uEAj1lsBMhA== + dependencies: + "@cspotcode/source-map-consumer" "0.8.0" + "@discoveryjs/json-ext@^0.5.0": version "0.5.6" resolved "https://registry.yarnpkg.com/@discoveryjs/json-ext/-/json-ext-0.5.6.tgz#d5e0706cf8c6acd8c6032f8d54070af261bbbb2f" @@ -808,6 +847,26 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" integrity sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw== +"@tsconfig/node10@^1.0.7": + version "1.0.8" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.8.tgz#c1e4e80d6f964fbecb3359c43bd48b40f7cadad9" + integrity sha512-6XFfSQmMgq0CFLY1MslA/CPUfhIL919M1rMsa5lP2P097N2Wd1sSX0tx1u4olM16fLNhtHZpRhedZJphNJqmZg== + +"@tsconfig/node12@^1.0.7": + version "1.0.9" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.9.tgz#62c1f6dee2ebd9aead80dc3afa56810e58e1a04c" + integrity sha512-/yBMcem+fbvhSREH+s14YJi18sp7J9jpuhYByADT2rypfajMZZN4WQ6zBGgBKp53NKmqI36wFYDb3yaMPurITw== + +"@tsconfig/node14@^1.0.0": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.1.tgz#95f2d167ffb9b8d2068b0b235302fafd4df711f2" + integrity sha512-509r2+yARFfHHE7T6Puu2jjkoycftovhXRqW328PDXTVGKihlb1P8Z9mMZH04ebyajfRY7dedfGynlrFHJUQCg== + +"@tsconfig/node16@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.2.tgz#423c77877d0569db20e1fc80885ac4118314010e" + integrity sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA== + "@types/babel__core@^7.0.0", "@types/babel__core@^7.1.14": version "7.1.18" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.18.tgz#1a29abcc411a9c05e2094c98f9a1b7da6cdf49f8" @@ -1177,6 +1236,11 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.1.1: + version "8.2.0" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.2.0.tgz#741210f2e2426454508853a2f44d0ab83b7f69c1" + integrity sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA== + acorn@8.5.0: version "8.5.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.5.0.tgz#4512ccb99b3698c752591e9bb4472e38ad43cee2" @@ -1268,6 +1332,11 @@ anymatch@^3.0.3, anymatch@~3.1.1, anymatch@~3.1.2: normalize-path "^3.0.0" picomatch "^2.0.4" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^1.0.7: version "1.0.10" resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" @@ -1699,6 +1768,18 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== +chevrotain@^10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/chevrotain/-/chevrotain-10.0.0.tgz#f6807694de85a384d9bfdf05925bedb6dd7455a0" + integrity sha512-tVAJWP7syUcmWZndXWRwOtFXIEX/+ToxQHMeO/NslsVmyeZhYngEeoSosJFojvBM+EhScj3nVThP6kBf3xhfsw== + dependencies: + "@chevrotain/cst-dts-gen" "^10.0.0" + "@chevrotain/gast" "^10.0.0" + "@chevrotain/types" "^10.0.0" + "@chevrotain/utils" "^10.0.0" + lodash "4.17.21" + regexp-to-ast "0.5.0" + chokidar@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.1.tgz#ee9ce7bbebd2b79f49f304799d5468e31e14e68a" @@ -1915,6 +1996,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" @@ -2089,6 +2175,11 @@ diff-sequences@^27.4.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-27.4.0.tgz#d783920ad8d06ec718a060d00196dfef25b132a5" integrity sha512-YqiQzkrsmHMH5uuh8OdQFU9/ZpADnwzml8z0O5HvRNda+5UZsaX/xN+AAxfR2hWq1Y7HZnAzO9J5lJXOuDz2Ww== +diff@^4.0.1: + version "4.0.2" + resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" + integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -3882,7 +3973,7 @@ lodash.sortby@^4.7.0: resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg= -lodash@^4.17.21, lodash@^4.7.0: +lodash@4.17.21, lodash@^4.17.21, lodash@^4.7.0: version "4.17.21" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== @@ -3915,6 +4006,11 @@ make-dir@^3.0.0, make-dir@^3.0.2: dependencies: semver "^6.0.0" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + makeerror@1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/makeerror/-/makeerror-1.0.12.tgz#3e5dd2079a82e812e983cc6610c4a2cb0eaa801a" @@ -4740,6 +4836,11 @@ regenerator-runtime@^0.13.4: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== +regexp-to-ast@0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz#56c73856bee5e1fef7f73a00f1473452ab712a24" + integrity sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw== + regexp.prototype.flags@^1.2.0: version "1.4.1" resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.4.1.tgz#b3f4c0059af9e47eca9f3f660e51d81307e72307" @@ -5333,6 +5434,25 @@ ts-loader@^9.2.6: micromatch "^4.0.0" semver "^7.3.4" +ts-node@^10.5.0: + version "10.5.0" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.5.0.tgz#618bef5854c1fbbedf5e31465cbb224a1d524ef9" + integrity sha512-6kEJKwVxAJ35W4akuiysfKwKmjkbYxwQMTBaAxo9KKAx/Yd26mPUyhGz3ji+EsJoAgrLqVsYHNuuYwQe22lbtw== + dependencies: + "@cspotcode/source-map-support" "0.7.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.0" + yn "3.1.1" + tsconfig-paths@^3.11.0, tsconfig-paths@^3.9.0: version "3.12.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" @@ -5491,6 +5611,11 @@ uuid@^3.4.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== +v8-compile-cache-lib@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.0.tgz#0582bcb1c74f3a2ee46487ceecf372e46bce53e8" + integrity sha512-mpSYqfsFvASnSn5qMiwrr4VKfumbPyONLCOPmsR3A6pTY/r0+tSaVbgPWSAIuzbk3lCTa+FForeTiO+wBQGkjA== + v8-compile-cache@^2.0.3: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -5772,6 +5897,11 @@ yargs@^16.2.0: y18n "^5.0.5" yargs-parser "^20.2.2" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"