Skip to content

Commit

Permalink
Make parser feedback easier to understand
Browse files Browse the repository at this point in the history
* add en-informal and de-informal localization
* replace expected token types with token values in parser errors
* move keywords to localization data
* fix parser ignoring trailing asterisc statements
* move Exception class out of localization
  • Loading branch information
philer committed Nov 29, 2021
1 parent 3eb890c commit aab3873
Show file tree
Hide file tree
Showing 20 changed files with 438 additions and 272 deletions.
2 changes: 1 addition & 1 deletion config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ config({
// Must be available as a localization/*.js file.
// Use an array to specify fallback options, e.g. ["de", "en"]
// Use "auto" to detect browser locale.
locale: ["auto", "en"],
locale: ["auto-informal", "auto", "en"],

// graphics sprite themes
tile_theme: "themes/neoz7",
Expand Down
41 changes: 41 additions & 0 deletions localization/de-informal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Kids friendly German localization variables.
* Enable by setting locale: "de-informal" in config.js.
*/
config({
simulation: {
run: "Los!",
},
error: {
parser: {
token_read:
"Ich verstehe nicht, was du in Zeile {line} meinst.",
unexpected_eof:
"Das Programm sollte noch weitergehen.",
unexpected_eof_instead:
"Das Programm sollte noch weitergehen. Als nächstes würde ich {expected} schreiben.",
unexpected_token:
"Ich verstehe nicht, was du mit '{value}' in Zeile {line} meinst.",
unexpected_token_instead:
"Ich verstehe nicht, was was du in Zeile {line} meinst. Du hast '{value}' geschrieben, aber ich habe {expected} erwartet.",
nested_program_definition:
"Ein Programm darf nicht in einem anderen Block stehen.",
nested_routine_definition:
"Eine Anweisung darf nicht in einem anderen Block stehen.",
},
runtime: {
undefined: "Den Befehl '{identifier}' kenne ich nicht.",
},
world: {
move_out_of_world: "Ich kann nicht über den Rand meiner Welt laufen.",
jump_too_high: "So hoch kann ich nicht springen.",
move_cuboid: "Auf einen Quader kann ich mich nicht stellen.",
action_out_of_world: "Ich kann Ziegel nicht aus meiner Welt hinauswerfen.",
action_cuboid: "Auf einen Quader kann ich keine Ziegel legen.",
action_too_high: "Ich kann nicht höher stapeln.",
action_no_blocks: "Hier gibt es keine Ziegel zum aufheben.",
action_already_marked: "Das Feld ist schon markiert.",
action_no_mark: "Hier gibt es keine Markierung zum löschen.",
},
},
})
23 changes: 20 additions & 3 deletions localization/de.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@
* Enable by setting locale: "de" in config.js.
*/
config({
welcome: "Willkommen! 👋🤖 Ich bin online Karol v{version}. Du kannst auch eine {older_release} ausprobieren.",
welcome: "Hallo! 👋🤖 Ich bin online Karol Version {version}. Du kannst auch eine {older_release} ausprobieren.",
older_release: "ältere Version",
or: "oder",
program: {
code: "Programm",
save: "Speichern",
Expand Down Expand Up @@ -43,20 +44,36 @@ config({
turnRight: "Nach rechts drehen",
},
},
language: {
IF: "wenn",
THEN: "dann",
ELSE: "sonst",
WHILE: "solange",
DO: "tue",
NOT: "nicht",
REPEAT: "wiederhole",
TIMES: "mal",
PROGRAM: "programm",
ROUTINE: "anweisung",
},
error: {
browser_feature_not_available: "Der Browser ist veraltet und unterstützt diese Funktionalität nicht.",
invalid_world_file: "Das ist keine valide *.kdw Datei.",
parser: {
token_read:
"Syntax-Fehler in Zeile {line}, Spalte {column}: Nächstes Wort nicht lesbar.",
unexpected_eof:
"Lese-Fehler: Unerwartetes End der Eingabe.",
unexpected_eof_instead:
"Lese-Fehler: Unerwartetes End der Eingabe, erwarte stattdessen {expected}.",
unexpected_token:
"Lese-Fehler in Zeile {line}, Spalte {column}: Unerwartes Wort '{value}'.",
unexpected_token_instead:
"Lese-Fehler in Zeile {line}, Spalte {column}: Unerwartes Wort '{value}', erwarte stattdessen {expected} .",
nested_program_definition:
"Lese-Fehler in Zeile {}: Programm in verschachteltem Kontext nicht definierbar.",
"Lese-Fehler in Zeile {line}: Programm in verschachteltem Kontext nicht definierbar.",
nested_routine_definition:
"Lese-Fehler in Zeile {}: Anweisung in verschachteltem Kontext nicht definierbar.",
"Lese-Fehler in Zeile {line}: Anweisung in verschachteltem Kontext nicht definierbar.",
},
runtime: {
undefined: "Laufzeit-Fehler in Zeile {line}: {identifier} nicht definiert.",
Expand Down
41 changes: 41 additions & 0 deletions localization/en-informal.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Kids friendly English localization variables.
* Enable by setting locale: "de" in config.js.
*/
config({
simulation: {
run: "Go!",
},
error: {
parser: {
token_read:
"I don't understand what you mean on line {line}.",
unexpected_eof:
"The program isn't finished yet.",
unexpected_eof_instead:
"The program isn't finished yet. Next I'd write {expected}.",
unexpected_token:
"I don't understand what you mean by '{value}' on line {line}.",
unexpected_token_instead:
"I don't understand what you mean on line {line}. You wrote '{value}', but I was expecting {expected}.",
nested_program_definition:
"A Programm can't be inside another block.",
nested_routine_definition:
"A Routine can't be inside another block.",
},
runtime: {
undefined: "I don't know the command '{identifier}'.",
},
world: {
move_out_of_world: "I can't walk over the edge of my world.",
jump_too_high: "I can't jump that high.",
move_cuboid: "I can't stand on a cuboid.",
action_out_of_world: "I can't throw bricks out of my world.",
action_cuboid: "I can't place bricks on top of a cuboid.",
action_too_high: "I can't stack any higher.",
action_no_blocks: "There are no bricks here to pick up.",
action_already_marked: "This field is already marked.",
action_no_mark: "There is no mark here to remove.",
},
},
})
25 changes: 21 additions & 4 deletions localization/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@
* and serve as reference for other languages.
*/
config({
welcome: "Welcome! 👋🤖 I am online Karol v{version}. You can also try an {older_release}.",
welcome: "Welcome! 👋🤖 I am online Karol version {version}. You can also try an {older_release}.",
older_release: "older release",
or: "or",
program: {
code: "Code",
save: "Save",
Expand Down Expand Up @@ -46,20 +47,36 @@ config({
turnRight: "Turn right",
},
},
language: {
IF: "if",
THEN: "then",
ELSE: "else",
WHILE: "while",
DO: "do",
NOT: "not",
REPEAT: "repeat",
TIMES: "times",
PROGRAM: "program",
ROUTINE: "routine",
},
error: {
browser_feature_not_available: "Your browser does not support his feature. Consider switch to an up-to-date browser.",
invalid_world_file: "This does not appear to be a valid *.kdw file.",
parser: {
token_read:
"Syntax Error on line {line}, column {column}: Could not read next token.",
unexpected_eof:
"Parse Error: Unexpected end of input.",
unexpected_eof_instead:
"Parse Error: Unexpected end of input, was expecting {expected}",
unexpected_token:
"Parse Error on line {line}, column {column}: Unexpected token '{value}'.",
unexpected_token_instead:
"Parse Error on line {line}, column {column}: Unexpected token '{value}', was expecting {expected} .",
"Parse Error on line {line}, column {column}: Unexpected token '{value}', was expecting {expected}.",
nested_program_definition:
"Parse Error on line {}: Can't define program in nested context.",
"Parse Error on line {line}: Can't define program in nested context.",
nested_routine_definition:
"Parse Error on line {}: Can't define routine in nested context.",
"Parse Error on line {line}: Can't define routine in nested context.",
},
runtime: {
undefined: "Runtime Error on line {line}: {identifier} is not defined.",
Expand Down
8 changes: 4 additions & 4 deletions src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import {h, render} from "preact"
import {useContext, useEffect, useErrorBoundary, useState} from "preact/hooks"

import "./global.css"
import * as graphics from "./graphics"
import {init as initGraphics} from "./graphics"
import {init as initLocalization, translate as t} from "./localization"
import * as editor from "./ui/Editor"
import {init as initEditor} from "./ui/Editor"
import {Main} from "./ui/Main"
import {Logging, LoggingProvider} from "./ui/Logging"
import {Translate} from "./ui/Translate"
Expand All @@ -15,9 +15,9 @@ import * as style from "./App.module.css"


const initPromises = Promise.all([
graphics.init(),
initGraphics(),
initEditor(),
initLocalization(),
editor.loadTheme(),
])


Expand Down
13 changes: 13 additions & 0 deletions src/exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Errors for our simulated programming language and runtime.
* These do not inherit from JS's own Error as they do not need
* to reveal details of the interpreter/runtime internals.
*/
export class Exception {
message: string
data: any[]
constructor(message: string, ...data: any[]) {
this.message = message
this.data = data
}
}
2 changes: 1 addition & 1 deletion src/language/highlight.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Exception} from "../localization"
import {Exception} from "../exception"
import {clsx} from "../util"

import * as tokens from "./tokens"
Expand Down
2 changes: 1 addition & 1 deletion src/language/interpreter.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Exception} from "../localization"
import {Exception} from "../exception"
import * as tokens from "./tokens"
import {Call, Expression, RoutineDefinition, Sequence, Statement, textToAst} from "./parser"

Expand Down
62 changes: 35 additions & 27 deletions src/language/parser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {Exception} from "../localization"

import {Exception} from "../exception"
import {commaList} from "../util"
import {translate} from "../localization"
import * as tokens from "./tokens"
// eslint-disable-next-line no-duplicate-imports
import {Token, TokenType, tokenize} from "./tokens"
import {Token, TokenType, tokenTypeToLiteral, tokenize} from "./tokens"


export interface AbstractStatement {
type: TokenType
Expand Down Expand Up @@ -86,10 +88,7 @@ export class Parser {
this.forward()
return type
} else {
throw new Exception("error.parser.unexpected_token_instead", {
...this.currentToken,
expected: types,
})
throw error("error.parser.unexpected_token_instead", this.currentToken, types)
}
}

Expand All @@ -108,10 +107,7 @@ export class Parser {
this.forward()
return value
}
throw new Exception("error.parser.unexpected_token_instead", {
...this.currentToken,
expected: types,
})
throw error("error.parser.unexpected_token_instead", this.currentToken, types)
}

readExpression(): Expression {
Expand All @@ -138,7 +134,7 @@ export class Parser {
return expr
}
}
throw new Exception("error.parser.unexpected_token", this.currentToken)
throw error("error.parser.unexpected_token", this.currentToken)
}

readCall(): Call {
Expand All @@ -158,9 +154,8 @@ export class Parser {
return call
}

readSequence(): Sequence {
readSequence(...endTokens: [TokenType, ...TokenType[]]): Sequence {
const statements: Sequence = []
const endTokens: TokenType[] = [tokens.ASTERISC, tokens.ELSE, tokens.EOF]
this.depth++
while (!endTokens.includes(this.currentToken.type)) {
statements.push(this.readStatement())
Expand All @@ -180,11 +175,11 @@ export class Parser {
this.forward()
const condition = this.readExpression()
this.eat(tokens.THEN)
const sequence = this.readSequence()
const sequence = this.readSequence(tokens.ELSE, tokens.ASTERISC)
const statement: IfStatement = {type, line, condition, sequence}
if (this.currentToken.type === tokens.ELSE) {
this.forward()
statement.alternative = this.readSequence()
statement.alternative = this.readSequence(tokens.ASTERISC)
}
this.eat(tokens.ASTERISC)
this.eat(tokens.IF)
Expand All @@ -195,7 +190,7 @@ export class Parser {
this.forward()
const condition = this.readExpression()
this.eat(tokens.DO)
const sequence = this.readSequence()
const sequence = this.readSequence(tokens.ASTERISC)
this.eat(tokens.ASTERISC)
this.eat(tokens.WHILE)
return {type, line, condition, sequence}
Expand All @@ -211,12 +206,12 @@ export class Parser {
type: tokens.WHILE,
line,
condition: this.readExpression(),
sequence: this.readSequence(),
sequence: this.readSequence(tokens.ASTERISC),
}
} else {
const count = this.readExpression()
this.eat(tokens.TIMES)
statement = {type, line, count, sequence: this.readSequence()}
statement = {type, line, count, sequence: this.readSequence(tokens.ASTERISC)}
}
this.eat(tokens.ASTERISC)
this.eat(tokens.REPEAT)
Expand All @@ -225,20 +220,18 @@ export class Parser {

case tokens.PROGRAM: {
if (this.depth > 1) {
throw new Exception("error.parser.nested_program_definition",
this.currentToken.line)
throw error("error.parser.nested_program_definition", this.currentToken)
}
this.forward()
const sequence = this.readSequence()
const sequence = this.readSequence(tokens.ASTERISC)
this.eat(tokens.ASTERISC)
this.eat(tokens.PROGRAM)
return {type, line, sequence}
}

case tokens.ROUTINE: {
if (this.depth > 1) {
throw new Exception("error.parser.nested_program_definition",
this.currentToken.line)
throw error("error.parser.nested_program_definition", this.currentToken)
}
this.forward()
const identifier = this.readToken(tokens.IDENTIFIER)
Expand All @@ -249,16 +242,31 @@ export class Parser {
argNames.push(this.readToken(tokens.IDENTIFIER))
}
}
const sequence = this.readSequence()
const sequence = this.readSequence(tokens.ASTERISC)
this.eat(tokens.ASTERISC)
this.eat(tokens.ROUTINE)
return {type, line, identifier, argNames, sequence}
}
}
throw new Exception("error.parser.unexpected_token", this.currentToken)
throw error("error.parser.unexpected_token", this.currentToken)
}
}


function error(key: string, token: Token, expected: TokenType[] = []) {
if (token.type === tokens.EOF) {
key = expected.length ? "error.parser.unexpected_eof_instead" : "error.parser.unexpected_eof"
}
return new Exception(key, {
...token,
expected: commaList(
expected.map(tt => `'${tokenTypeToLiteral.get(tt) || tt}'`),
` ${translate("or")} `,
),
})
}


/** Convenience funtion turns code into an abstract syntax tree. */
export const textToAst = (text: string) =>
new Parser(tokenize(text)).readSequence()
new Parser(tokenize(text)).readSequence(tokens.EOF)
Loading

0 comments on commit aab3873

Please sign in to comment.