Improved error handling for most calculations
This commit is contained in:
@@ -4,6 +4,6 @@ export default function embedInlineCalculations(string, calculations){
|
||||
let index = 0;
|
||||
return string.replace(/\{([^{}]*)\}/g, () => {
|
||||
let comp = calculations && calculations[index++];
|
||||
return comp && comp.result;
|
||||
return comp && comp.result ? comp.result : string;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18,11 +18,6 @@ export default function evaluateString(string, scope, fn = 'compile'){
|
||||
errors.push(e);
|
||||
return {result: string, errors};
|
||||
}
|
||||
// Parsing failed
|
||||
if (node === null){
|
||||
errors.push('...');
|
||||
return {result: string, errors};
|
||||
}
|
||||
let context = new CompilationContext();
|
||||
let result = node[fn](scope, context);
|
||||
if (result instanceof ConstantNode){
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import evaluateCalculation from '/imports/api/creature/computation/engine/evaluateCalculation.js';
|
||||
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
|
||||
import { union } from 'lodash';
|
||||
|
||||
export default function computeInlineCalculations(prop, memo){
|
||||
@@ -21,9 +22,12 @@ function computeInlineCalcsForField(prop, memo, field){
|
||||
context,
|
||||
dependencies,
|
||||
} = evaluateCalculation({string: calculation, prop, memo, fn: 'compile'});
|
||||
if (result instanceof ErrorNode){
|
||||
result = `<code>${result.toString()}</code>`;
|
||||
}
|
||||
let computation = {
|
||||
calculation,
|
||||
result: result.toString(),
|
||||
result: result && result.toString(),
|
||||
};
|
||||
if (context.errors.length){
|
||||
computation.errors = context.errors;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import computeStat from '/imports/api/creature/computation/engine/computeStat.js';
|
||||
import { parse, CompilationContext } from '/imports/parser/parser.js';
|
||||
import { prettifyParseError, parse, CompilationContext } from '/imports/parser/parser.js';
|
||||
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
|
||||
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
|
||||
import ConstantNode from '/imports/parser/parseTree/ConstantNode.js';
|
||||
import ErrorNode from '/imports/parser/parseTree/ErrorNode.js';
|
||||
import findAncestorByType from '/imports/api/creature/computation/engine/findAncestorByType.js';
|
||||
import { union } from 'lodash';
|
||||
|
||||
@@ -14,10 +15,10 @@ export default function evaluateCalculation({
|
||||
fn = 'reduce',
|
||||
}){
|
||||
let dependencies = [];
|
||||
let errors = [];
|
||||
let context = new CompilationContext();
|
||||
if (!string) return {
|
||||
context: {errors},
|
||||
result: new ConstantNode({value: string, type: 'string'}),
|
||||
context,
|
||||
dependencies,
|
||||
};
|
||||
// Parse the string
|
||||
@@ -25,34 +26,24 @@ export default function evaluateCalculation({
|
||||
try {
|
||||
calc = parse(string);
|
||||
} catch (e) {
|
||||
errors.push({
|
||||
type: 'parsing',
|
||||
message: e.message || e
|
||||
});
|
||||
let error = prettifyParseError(e);
|
||||
return {
|
||||
context: {errors},
|
||||
result: new ConstantNode({value: string, type: 'string'}),
|
||||
dependencies,
|
||||
};
|
||||
}
|
||||
if (!calc){
|
||||
return {
|
||||
context: {errors},
|
||||
result: new ConstantNode({value: calc, type: 'string'}),
|
||||
result: new ErrorNode({context, error}),
|
||||
context,
|
||||
dependencies,
|
||||
};
|
||||
}
|
||||
|
||||
// Replace constants with their parsed constant
|
||||
let replaceResults = replaceConstants({
|
||||
calc, memo, prop, dependencies, errors
|
||||
calc, memo, prop, dependencies, context
|
||||
});
|
||||
dependencies = replaceResults.dependencies;
|
||||
calc = replaceResults.calc;
|
||||
if (replaceResults.failed){
|
||||
return {
|
||||
context: {errors},
|
||||
result: new ConstantNode({value: string, type: 'string'}),
|
||||
context,
|
||||
dependencies,
|
||||
};
|
||||
}
|
||||
@@ -61,13 +52,12 @@ export default function evaluateCalculation({
|
||||
dependencies = computeSymbols({calc, memo, prop, dependencies})
|
||||
|
||||
// Evaluate
|
||||
let context = new CompilationContext();
|
||||
let result = calc[fn](memo.statsByVariableName, context);
|
||||
return {result, context, dependencies};
|
||||
}
|
||||
|
||||
// Replace constants in the calc with the right ParseNodes
|
||||
function replaceConstants({calc, memo, prop, dependencies, errors}){
|
||||
function replaceConstants({calc, memo, prop, dependencies, context}){
|
||||
let constFailed = [];
|
||||
calc = calc.replaceNodes(node => {
|
||||
if (!(node instanceof SymbolNode)) return;
|
||||
@@ -101,16 +91,16 @@ function replaceConstants({calc, memo, prop, dependencies, errors}){
|
||||
}
|
||||
});
|
||||
constFailed.forEach(name => {
|
||||
errors.push({
|
||||
context.storeError({
|
||||
type: 'error',
|
||||
message: `${name} is a constant property with parsing errors`
|
||||
});
|
||||
});
|
||||
return {
|
||||
failed: !!constFailed.length,
|
||||
dependencies,
|
||||
calc,
|
||||
};
|
||||
let failed = !!constFailed.length;
|
||||
if (failed){
|
||||
calc = new ErrorNode({error: 'Failed to replace constants'});
|
||||
}
|
||||
return { failed, dependencies, calc };
|
||||
}
|
||||
|
||||
// Ensure all symbol nodes are defined and computed
|
||||
|
||||
@@ -4,7 +4,11 @@ import LogContentSchema from '/imports/api/creature/log/LogContentSchema.js';
|
||||
import { ValidatedMethod } from 'meteor/mdg:validated-method';
|
||||
import { RateLimiterMixin } from 'ddp-rate-limiter-mixin';
|
||||
import {assertEditPermission} from '/imports/api/creature/creaturePermissions.js';
|
||||
import { parse, CompilationContext } from '/imports/parser/parser.js';
|
||||
import {
|
||||
parse,
|
||||
CompilationContext,
|
||||
prettifyParseError
|
||||
} from '/imports/parser/parser.js';
|
||||
const PER_CREATURE_LOG_LIMIT = 100;
|
||||
|
||||
if (Meteor.isServer){
|
||||
@@ -149,13 +153,15 @@ const logRoll = new ValidatedMethod({
|
||||
avatarPicture: 1,
|
||||
}});
|
||||
assertEditPermission(creature, this.userId);
|
||||
let parsedResult = parse(roll);
|
||||
let logContent;
|
||||
if (parsedResult === null) {
|
||||
logContent = [{error: 'Unexpected end of input'}];
|
||||
let logContent = []
|
||||
let parsedResult = undefined;
|
||||
try {
|
||||
parsedResult = parse(roll);
|
||||
} catch (e){
|
||||
let error = prettifyParseError(e);
|
||||
logContent.push({error});
|
||||
}
|
||||
else try {
|
||||
logContent = [];
|
||||
if (parsedResult) try {
|
||||
let rollContext = new CompilationContext();
|
||||
let compiled = parsedResult.compile(creature.variables, rollContext);
|
||||
let compiledString = compiled.toString();
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import SimpleSchema from 'simpl-schema';
|
||||
import VARIABLE_NAME_REGEX from '/imports/constants/VARIABLE_NAME_REGEX.js';
|
||||
import ErrorSchema from '/imports/api/properties/subSchemas/ErrorSchema.js';
|
||||
import { parse, CompilationContext } from '/imports/parser/parser.js';
|
||||
import {
|
||||
parse,
|
||||
CompilationContext,
|
||||
prettifyParseError,
|
||||
} from '/imports/parser/parser.js';
|
||||
import AccessorNode from '/imports/parser/parseTree/AccessorNode.js';
|
||||
import SymbolNode from '/imports/parser/parseTree/SymbolNode.js';
|
||||
/*
|
||||
@@ -35,19 +39,19 @@ let ConstantSchema = new SimpleSchema({
|
||||
}
|
||||
let string = calc.value;
|
||||
// Evaluate the calculation with no scope
|
||||
let {result, errors} = parseString(string);
|
||||
// Any errors will result in a failure
|
||||
if (errors.length) return errors;
|
||||
let {result, context} = parseString(string);
|
||||
// Any existing errors will result in an early failure
|
||||
if (context.errors.length) return context.errors;
|
||||
// Ban variables in constants if necessary
|
||||
result && result.traverse(node => {
|
||||
if (node instanceof SymbolNode || node instanceof AccessorNode){
|
||||
errors.push({
|
||||
context.storeError()({
|
||||
type: 'error',
|
||||
message: 'Variables can\'t be used to define a constant'
|
||||
});
|
||||
}
|
||||
});
|
||||
return errors;
|
||||
return context.errors;
|
||||
}
|
||||
},
|
||||
'errors.$':{
|
||||
@@ -56,9 +60,9 @@ let ConstantSchema = new SimpleSchema({
|
||||
});
|
||||
|
||||
function parseString(string, fn = 'compile'){
|
||||
let errors = [];
|
||||
let context = new CompilationContext();
|
||||
if (!string){
|
||||
return {result: string, errors};
|
||||
return {result: string, errors: []};
|
||||
}
|
||||
|
||||
// Parse the string using mathjs
|
||||
@@ -66,18 +70,12 @@ function parseString(string, fn = 'compile'){
|
||||
try {
|
||||
node = parse(string);
|
||||
} catch (e) {
|
||||
let message = e.toString().split('.')[0];
|
||||
errors.push({type: 'error', message});
|
||||
return {result: string, errors};
|
||||
let message = prettifyParseError(e);
|
||||
context.storeError({type: 'error', message});
|
||||
return {result: string, errors: context.errors};
|
||||
}
|
||||
// Parsing incomplete
|
||||
if (node === null){
|
||||
errors.push({type: 'warning', message: 'Unexpected end of input'});
|
||||
return {result: string, errors};
|
||||
}
|
||||
let context = new CompilationContext();
|
||||
let result = node[fn]({/*empty scope*/}, context);
|
||||
return {result, errors: context.errors}
|
||||
return {result, context}
|
||||
}
|
||||
|
||||
export { ConstantSchema };
|
||||
|
||||
@@ -16,6 +16,6 @@ export default class ErrorNode extends ParseNode {
|
||||
return this;
|
||||
}
|
||||
toString(){
|
||||
return '###';
|
||||
return this.error.toString();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,10 +28,21 @@ export function parse(string){
|
||||
if (results.length === 1){
|
||||
return results[0];
|
||||
} else if (results.length === 0){
|
||||
// Valid parsing up until now, but need more. Unexpected end of input.
|
||||
return null;
|
||||
// Valid parsing up until now, but need more
|
||||
throw new EndOfInputError('Unexpected end of input');
|
||||
} else {
|
||||
console.warn('Grammar is ambiguous!', {string, results});
|
||||
return results[0];
|
||||
}
|
||||
}
|
||||
|
||||
export function prettifyParseError(e){
|
||||
if (e.message) e = e.message
|
||||
return e.toString().split('.')[0];
|
||||
}
|
||||
|
||||
class EndOfInputError extends Error {
|
||||
constructor(message = '', ...args) {
|
||||
super(message, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,8 +244,13 @@ export default {
|
||||
nodes = nodes.filter(node => {
|
||||
if (node.slotFillerCondition){
|
||||
let context = new CompilationContext();
|
||||
let conditionResult = parse(node.slotFillerCondition)
|
||||
.reduce(this.creature.variables, context);
|
||||
let conditionResult;
|
||||
try {
|
||||
conditionResult = parse(node.slotFillerCondition)
|
||||
.reduce(this.creature.variables, context);
|
||||
} catch (e){
|
||||
console.warn(e);
|
||||
}
|
||||
if (conditionResult && !conditionResult.value) return false;
|
||||
}
|
||||
if (
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
import CreatureLogs, { logRoll } from '/imports/api/creature/log/CreatureLogs.js';
|
||||
import Creatures from '/imports/api/creature/Creatures.js';
|
||||
import { assertEditPermission } from '/imports/api/creature/creaturePermissions.js';
|
||||
import { parse } from '/imports/parser/parser.js';
|
||||
import { parse, prettifyParseError } from '/imports/parser/parser.js';
|
||||
import LogEntry from '/imports/ui/log/LogEntry.vue';
|
||||
|
||||
export default {
|
||||
@@ -61,12 +61,12 @@ export default {
|
||||
try {
|
||||
result = parse(value);
|
||||
} catch (e){
|
||||
console.error(e);
|
||||
this.inputError = 'Invalid syntax';
|
||||
return;
|
||||
}
|
||||
if (result === null){
|
||||
this.inputError = '...';
|
||||
if (e.constructor.name === 'EndOfInputError'){
|
||||
this.inputError = '...';
|
||||
} else {
|
||||
let error = prettifyParseError(e);
|
||||
this.inputError = error;
|
||||
}
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@@ -74,7 +74,7 @@ export default {
|
||||
this.inputHint = compiled.toString();
|
||||
return;
|
||||
} catch (e){
|
||||
console.error(e);
|
||||
console.warn(e);
|
||||
this.inputError = 'Compilation error';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<template lang="html">
|
||||
<div class="layout column align-center justify-center pa-4">
|
||||
<v-card style="width: 100%; max-width: 400px;">
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="input"
|
||||
label="input"
|
||||
/>
|
||||
<v-btn
|
||||
icon
|
||||
@click="recompute"
|
||||
>
|
||||
<v-icon>refresh</v-icon>
|
||||
</v-btn>
|
||||
<v-textarea
|
||||
v-model="output"
|
||||
readonly
|
||||
label="output"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="string"
|
||||
readonly
|
||||
label="string"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="compiled"
|
||||
readonly
|
||||
label="compiled"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="rolled"
|
||||
readonly
|
||||
label="rolled"
|
||||
/>
|
||||
<v-text-field
|
||||
v-model="reduced"
|
||||
readonly
|
||||
label="reduced"
|
||||
/>
|
||||
<v-textarea
|
||||
v-model="reducedJson"
|
||||
readonly
|
||||
label="reduced"
|
||||
/>
|
||||
<v-textarea
|
||||
:value="contextJSON"
|
||||
readonly
|
||||
label="reduced"
|
||||
/>
|
||||
<function-reference />
|
||||
<v-dialog
|
||||
width="500"
|
||||
>
|
||||
<template #activator="{ on }">
|
||||
<v-btn
|
||||
color="red lighten-2"
|
||||
dark
|
||||
v-on="on"
|
||||
>
|
||||
Click Me
|
||||
</v-btn>
|
||||
</template>
|
||||
<check :bonus="4" />
|
||||
</v-dialog>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { parse, CompilationContext } from '/imports/parser/parser.js';
|
||||
import FunctionReference from '/imports/ui/documentation/FunctionReference.vue';
|
||||
import Check from '/imports/ui/components/rolls/Check.vue';
|
||||
console.log(parse);
|
||||
export default {
|
||||
components: {
|
||||
FunctionReference,
|
||||
Check,
|
||||
},
|
||||
data(){return {
|
||||
input: null,
|
||||
output: null,
|
||||
compiled: null,
|
||||
string: null,
|
||||
rolled: null,
|
||||
reduced: null,
|
||||
reducedJson: null,
|
||||
context: null,
|
||||
}},
|
||||
computed: {
|
||||
contextJSON(){
|
||||
return JSON.stringify(this.context, null, 2);
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
input(){
|
||||
this.recompute();
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
recompute(){
|
||||
let val = this.input;
|
||||
this.output = this.compiled = this.string = '';
|
||||
let output = parse(val);
|
||||
console.log(output);
|
||||
this.output = JSON.stringify(output, null, 2);
|
||||
if (!output) return;
|
||||
this.string = output;
|
||||
let scope = {strength: {value: 16}, hitpoints: {value: 32, currentValue: 8}, mouse: 3};
|
||||
this.context = new CompilationContext();
|
||||
this.compiled = output.compile(scope, this.context);
|
||||
this.rolled = this.compiled.roll(scope, this.context);
|
||||
this.reduced = this.rolled.reduce(scope, this.context);
|
||||
this.reducedJson = JSON.stringify(this.reduced, null, 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="css" scoped>
|
||||
</style>
|
||||
@@ -24,7 +24,6 @@ import PatreonLevelTooLow from '/imports/ui/pages/PatreonLevelTooLow.vue';
|
||||
import Tabletops from '/imports/ui/pages/Tabletops.vue';
|
||||
import Tabletop from '/imports/ui/pages/Tabletop.vue';
|
||||
import TabletopToolbar from '/imports/ui/tabletop/TabletopToolbar.vue';
|
||||
import Parser from '/imports/ui/pages/Parser.vue';
|
||||
|
||||
let userSubscription = Meteor.subscribe('user');
|
||||
|
||||
@@ -111,14 +110,6 @@ RouterFactory.configure(factory => {
|
||||
title: 'Character List',
|
||||
},
|
||||
beforeEnter: ensureLoggedIn,
|
||||
},{
|
||||
path: '/parser',
|
||||
components: {
|
||||
default: Parser,
|
||||
},
|
||||
meta: {
|
||||
title: 'Parser',
|
||||
},
|
||||
},{
|
||||
path: '/library',
|
||||
components: {
|
||||
|
||||
Reference in New Issue
Block a user