mirror of
https://github.com/actualbudget/actual.git
synced 2026-04-28 10:33:02 -05:00
Deleting all unused files, deleting unused functions from loot-core (#1158)
Last one before I add the actual linter rules!
This commit is contained in:
@@ -1,65 +0,0 @@
|
||||
export function RepairSyncNotification() {}
|
||||
|
||||
// TODO: sync button shouldn't show error status if it's a local file
|
||||
// and needs uploading.. should just be grayed out
|
||||
//
|
||||
// TODO: improve styling of these modals
|
||||
|
||||
// export function NeedsUploadNotification({ actions }) {
|
||||
// let [loading, setLoading] = useState(false);
|
||||
|
||||
// return (
|
||||
// <Stack align="center" direction="row">
|
||||
// <Text>
|
||||
// This file is not a cloud file. You need to register it to take advantage
|
||||
// of syncing which allows you to use it across devices and never worry
|
||||
// about losing your data.
|
||||
// </Text>
|
||||
// <ButtonWithLoading
|
||||
// bare
|
||||
// loading={loading}
|
||||
// onClick={async () => {
|
||||
// setLoading(true);
|
||||
// await actions.uploadBudget();
|
||||
// actions.removeNotification('file-needs-upload');
|
||||
// setLoading(false);
|
||||
|
||||
// actions.sync();
|
||||
// actions.loadPrefs();
|
||||
// }}
|
||||
// style={{
|
||||
// backgroundColor: 'rgba(100, 100, 100, .12)',
|
||||
// color: colors.n1,
|
||||
// fontSize: 14,
|
||||
// flexShrink: 0,
|
||||
// '&:hover, &:active': { backgroundColor: 'rgba(100, 100, 100, .25)' }
|
||||
// }}
|
||||
// >
|
||||
// Register
|
||||
// </ButtonWithLoading>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
|
||||
// export function SyncResetNotification({ cloudFileId, actions }) {
|
||||
// return (
|
||||
// <Stack align="center" direction="row">
|
||||
// <Text>
|
||||
// </Text>
|
||||
// <Button
|
||||
// bare
|
||||
// onClick={async () => {
|
||||
// actions.removeNotification('out-of-date-key');
|
||||
// }}
|
||||
// style={{
|
||||
// backgroundColor: colors.r10,
|
||||
// flexShrink: 0,
|
||||
// '&:hover': { backgroundColor: colors.r10 },
|
||||
// '&:active': { backgroundColor: colors.r8 }
|
||||
// }}
|
||||
// >
|
||||
// Revert
|
||||
// </Button>
|
||||
// </Stack>
|
||||
// );
|
||||
// }
|
||||
@@ -1,143 +0,0 @@
|
||||
import React, { Component } from 'react';
|
||||
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { send } from 'loot-core/src/platform/client/fetch';
|
||||
|
||||
const Container = styled.div`
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
`;
|
||||
|
||||
const Code = styled.textarea`
|
||||
width: 100%;
|
||||
height: 10em;
|
||||
font-size: 1em;
|
||||
`;
|
||||
|
||||
const Output = styled.pre`
|
||||
width: 100%;
|
||||
background-color: #333333;
|
||||
color: white;
|
||||
padding: 0.5em;
|
||||
`;
|
||||
|
||||
class Debug extends Component {
|
||||
constructor() {
|
||||
super();
|
||||
this.state = {
|
||||
value: localStorage.debugValue,
|
||||
outputType: 'ast',
|
||||
ast: null,
|
||||
code: null,
|
||||
sql: null,
|
||||
sqlgenValue: localStorage.sqlgenValue,
|
||||
sqlgenRow: localStorage.sqlgenRow,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.fetchResults(this.state.value);
|
||||
this.fetchSqlGenResult(this.state.value);
|
||||
}
|
||||
|
||||
fetchResults(value) {
|
||||
localStorage.debugValue = value;
|
||||
|
||||
send('debug-ast', { code: value }).then(ast => {
|
||||
this.setState({ ast });
|
||||
});
|
||||
send('debug-code', { code: value }).then(code => {
|
||||
this.setState({ code });
|
||||
});
|
||||
send('debug-query', { code: value }).then(sql => {
|
||||
this.setState({ sql });
|
||||
});
|
||||
}
|
||||
|
||||
async fetchSqlGenResult() {
|
||||
let row = {};
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars, no-eval
|
||||
row = (0, eval)('(' + this.state.sqlgenRow + ')');
|
||||
} catch (e) {}
|
||||
|
||||
const res = await send('debug-sqlgen', {
|
||||
expr: this.state.sqlgenValue,
|
||||
});
|
||||
this.setState({ sqlgenResult: res });
|
||||
}
|
||||
|
||||
processInput(e) {
|
||||
this.setState({ value: e.target.value });
|
||||
this.fetchResults(e.target.value);
|
||||
}
|
||||
|
||||
processSqlGen(value, field) {
|
||||
localStorage[field] = value;
|
||||
this.setState({ [field]: value }, () => {
|
||||
this.fetchSqlGenResult();
|
||||
});
|
||||
}
|
||||
|
||||
onInputType(e) {
|
||||
this.setState({ outputType: e.target.value });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {
|
||||
// value,
|
||||
// outputType,
|
||||
// ast,
|
||||
// code,
|
||||
// sql,
|
||||
sqlgenValue,
|
||||
sqlgenRow,
|
||||
sqlgenResult,
|
||||
} = this.state;
|
||||
|
||||
return (
|
||||
<Container>
|
||||
{/*<h2>Debug</h2>
|
||||
<p>Input:</p>
|
||||
<Code value={value} onChange={this.processInput.bind(this)} />
|
||||
<select
|
||||
value={this.state.outputType}
|
||||
onChange={this.onInputType.bind(this)}
|
||||
>
|
||||
<option value="ast">AST</option>
|
||||
<option value="code">code</option>
|
||||
<option value="sql">SQL</option>
|
||||
</select>
|
||||
|
||||
<div style={{ display: outputType === 'ast' ? 'block' : 'none' }}>
|
||||
<p>AST:</p>
|
||||
<Output>{ast ? JSON.stringify(ast, null, 2) : ''}</Output>
|
||||
</div>
|
||||
|
||||
<div style={{ display: outputType === 'code' ? 'block' : 'none' }}>
|
||||
<p>Code:</p>
|
||||
<Output>{code || ''}</Output>
|
||||
</div>
|
||||
|
||||
<div style={{ display: outputType === 'sql' ? 'block' : 'none' }}>
|
||||
<p>SQL:</p>
|
||||
<Output>{sql || ''}</Output>
|
||||
</div>*/}
|
||||
|
||||
<h3>sqlgen</h3>
|
||||
<Code
|
||||
value={sqlgenValue}
|
||||
onChange={e => this.processSqlGen(e.target.value, 'sqlgenValue')}
|
||||
/>
|
||||
<Code
|
||||
value={sqlgenRow}
|
||||
onChange={e => this.processSqlGen(e.target.value, 'sqlgenRow')}
|
||||
/>
|
||||
<Output>{JSON.stringify(sqlgenResult)}</Output>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Debug;
|
||||
@@ -1,4 +0,0 @@
|
||||
export function getModalRoute(name: string): [string, string] {
|
||||
let parts = name.split('/');
|
||||
return [parts[0], parts.slice(1).join('/')];
|
||||
}
|
||||
@@ -1,82 +1,5 @@
|
||||
import * as models from './models';
|
||||
|
||||
export const transactionModel = {
|
||||
...models.transactionModel,
|
||||
|
||||
toExternal(transactions, idx, payees) {
|
||||
return transactions;
|
||||
// function convert(t, payee) {
|
||||
// return {
|
||||
// id: t.id,
|
||||
// account_id: t.acct,
|
||||
// amount: t.amount,
|
||||
// payee_id: payee ? payee.id : null,
|
||||
// payee: payee ? payee.name : null,
|
||||
// imported_payee: t.imported_description,
|
||||
// category_id: t.category,
|
||||
// date: t.date,
|
||||
// notes: t.notes,
|
||||
// imported_id: t.financial_id,
|
||||
// transfer_id: t.transferred_id,
|
||||
// cleared: t.cleared
|
||||
// };
|
||||
// }
|
||||
|
||||
// let splits = getAllSplitTransactions(transactions, idx);
|
||||
// if (splits) {
|
||||
// let payee =
|
||||
// splits.parent.description && payees[splits.parent.description];
|
||||
|
||||
// return {
|
||||
// ...convert(splits.parent, payee),
|
||||
// subtransactions: splits.children.map(child => convert(child, payee))
|
||||
// };
|
||||
// }
|
||||
|
||||
// let transaction = transactions[idx];
|
||||
// let payee = transaction.description && payees[transaction.description];
|
||||
// return convert(transaction, payee);
|
||||
},
|
||||
|
||||
fromExternal(transaction) {
|
||||
let result: Record<string, unknown> = {};
|
||||
if ('id' in transaction) {
|
||||
result.id = transaction.id;
|
||||
}
|
||||
if ('account_id' in transaction) {
|
||||
result.acct = transaction.account_id;
|
||||
}
|
||||
if ('amount' in transaction) {
|
||||
result.amount = transaction.amount;
|
||||
}
|
||||
if ('payee_id' in transaction) {
|
||||
result.description = transaction.payee_id;
|
||||
}
|
||||
if ('imported_payee' in transaction) {
|
||||
result.imported_description = transaction.imported_payee;
|
||||
}
|
||||
if ('category_id' in transaction) {
|
||||
result.category = transaction.category_id;
|
||||
}
|
||||
if ('date' in transaction) {
|
||||
result.date = transaction.date;
|
||||
}
|
||||
if ('notes' in transaction) {
|
||||
result.notes = transaction.notes;
|
||||
}
|
||||
if ('imported_id' in transaction) {
|
||||
result.financial_id = transaction.imported_id;
|
||||
}
|
||||
if ('transfer_id' in transaction) {
|
||||
result.transferred_id = transaction.transfer_id;
|
||||
}
|
||||
if ('cleared' in transaction) {
|
||||
result.cleared = transaction.cleared;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
};
|
||||
|
||||
export const accountModel = {
|
||||
...models.accountModel,
|
||||
|
||||
|
||||
@@ -1118,7 +1118,3 @@ export function generateSQLWithState(
|
||||
let { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig);
|
||||
return { sql: defaultConstructQuery(queryState, state, sqlPieces), state };
|
||||
}
|
||||
|
||||
export function generateSQL(queryState) {
|
||||
return generateSQLWithState(queryState).sql;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,3 @@ export function FileDownloadError(reason, meta?) {
|
||||
export function FileUploadError(reason, meta?) {
|
||||
return { type: 'FileUploadError', reason, meta };
|
||||
}
|
||||
|
||||
export function isCodeError(err) {
|
||||
return err instanceof ReferenceError || err instanceof SyntaxError;
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
let enabled = false;
|
||||
let entries = {};
|
||||
let counters = {};
|
||||
|
||||
export function reset() {
|
||||
entries = {};
|
||||
counters = {};
|
||||
}
|
||||
|
||||
export function record(name) {
|
||||
const start = Date.now();
|
||||
return () => unrecord(name, start);
|
||||
}
|
||||
|
||||
function unrecord(name, start) {
|
||||
const end = Date.now();
|
||||
|
||||
if (enabled) {
|
||||
if (entries[name] == null) {
|
||||
entries[name] = [];
|
||||
}
|
||||
entries[name].push(end - start);
|
||||
}
|
||||
}
|
||||
|
||||
export function increment(name) {
|
||||
if (enabled) {
|
||||
if (counters[name] == null) {
|
||||
counters[name] = 0;
|
||||
}
|
||||
counters[name]++;
|
||||
}
|
||||
}
|
||||
|
||||
export function start() {
|
||||
enabled = true;
|
||||
}
|
||||
|
||||
export function stop() {
|
||||
enabled = false;
|
||||
|
||||
console.log('~~ PERFORMANCE REPORT ~~');
|
||||
for (let name in entries) {
|
||||
const records = entries[name];
|
||||
const total = records.reduce((total, n) => total + n / 1000, 0);
|
||||
const avg = total / records.length;
|
||||
|
||||
console.log(
|
||||
`[${name}] count: ${records.length} total: ${total}s avg: ${avg}`,
|
||||
);
|
||||
}
|
||||
|
||||
for (let name in counters) {
|
||||
console.log(`[${name}] ${counters[name]}`);
|
||||
}
|
||||
console.log('~~ END REPORT ~~');
|
||||
|
||||
reset();
|
||||
}
|
||||
@@ -1,12 +1,3 @@
|
||||
export function first(arr) {
|
||||
return arr[0];
|
||||
}
|
||||
|
||||
export function firstValue(arr) {
|
||||
const keys = Object.keys(arr[0]);
|
||||
return arr[0][keys[0]];
|
||||
}
|
||||
|
||||
export function number(v) {
|
||||
if (typeof v === 'number') {
|
||||
return v;
|
||||
@@ -20,11 +11,3 @@ export function number(v) {
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
export function min(x, y) {
|
||||
return Math.min(x, y);
|
||||
}
|
||||
|
||||
export function max(x, y) {
|
||||
return Math.max(x, y);
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
* Function calls (native hooks)
|
||||
* Operators: + - / * > < >= <= =
|
||||
* Queries: from t in transactions select { amount }
|
||||
* Types
|
||||
** Boolean (true / false)
|
||||
** Integer
|
||||
** Float
|
||||
** String
|
||||
* Variables (only global lookup)
|
||||
|
||||
Need a stack to hold temporary values since function calls can be
|
||||
nested. Instructions:
|
||||
|
||||
MOV
|
||||
CALL
|
||||
QUERY
|
||||
BOP
|
||||
UOP
|
||||
|
||||
Registers:
|
||||
|
||||
PC
|
||||
SP
|
||||
REG1
|
||||
|
||||
Query language:
|
||||
|
||||
=from transactions
|
||||
where
|
||||
date >= 20170101 and
|
||||
date <= 20170131 and
|
||||
category.is_income = 1
|
||||
calculate sum(amount)
|
||||
@@ -1,734 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Compiler basic 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(query),
|
||||
"
|
||||
SELECT sum(amount) FROM transactions
|
||||
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
|
||||
LEFT JOIN accounts t1 ON t1.id = transactions.acct
|
||||
LEFT JOIN banks t2 ON t2.id = t1.bank
|
||||
WHERE ((((date >= 20170101) and (date <= 20170131)) and (t2.name = 1))) AND transactions.isParent = 0 AND transactions.tombstone = 0
|
||||
",
|
||||
true,
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(call),
|
||||
Object {
|
||||
"name": "generated!number",
|
||||
"type": "__var",
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(call),
|
||||
Object {
|
||||
"name": "generated!first",
|
||||
"type": "__var",
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler basic 2`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(mov),
|
||||
"",
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler compiler binary ops 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"name": "generated!bar",
|
||||
"type": "__var",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(bop),
|
||||
"+",
|
||||
Object {
|
||||
"name": "generated!foo",
|
||||
"type": "__var",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"name": "generated!baz",
|
||||
"type": "__var",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(bop),
|
||||
"+",
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"name": "generated!boo",
|
||||
"type": "__var",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(bop),
|
||||
"+",
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler compiler nested funcs 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(mov),
|
||||
0,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
-20000,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(call),
|
||||
Object {
|
||||
"name": "generated!number",
|
||||
"type": "__var",
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(call),
|
||||
Object {
|
||||
"name": "generated!min",
|
||||
"type": "__var",
|
||||
},
|
||||
Array [
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler compiles boolean types 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(mov),
|
||||
true,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
1,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(bop),
|
||||
"and",
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(jumpf),
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"get": [Function],
|
||||
"resolve": [Function],
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
0,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(jumpt),
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"get": [Function],
|
||||
"resolve": [Function],
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
1,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler complex query expressions 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(query),
|
||||
"
|
||||
SELECT substr(date,0,7), sum(amount) FROM transactions
|
||||
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
|
||||
|
||||
WHERE transactions.isParent = 0 AND transactions.tombstone = 0
|
||||
GROUP BY substr(date,0,7)",
|
||||
false,
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler field dependencies 1`] = `
|
||||
Array [
|
||||
"acct",
|
||||
"category",
|
||||
"description",
|
||||
"isParent",
|
||||
"tombstone",
|
||||
"date",
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler parens 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(mov),
|
||||
1,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
2,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(bop),
|
||||
"+",
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler parens 2`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(mov),
|
||||
1232,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
2,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(bop),
|
||||
"+",
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
3,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"index": 2,
|
||||
"type": "__stack",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
4,
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(bop),
|
||||
"+",
|
||||
Object {
|
||||
"index": 2,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(bop),
|
||||
"-",
|
||||
Object {
|
||||
"index": 0,
|
||||
"type": "__stack",
|
||||
},
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler query expressions 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(query),
|
||||
"
|
||||
SELECT sum(amount) as a FROM transactions
|
||||
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
|
||||
|
||||
WHERE ((amount > 0)) AND transactions.isParent = 0 AND transactions.tombstone = 0
|
||||
",
|
||||
false,
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler query expressions with field remapping 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(query),
|
||||
"
|
||||
SELECT id FROM transactions
|
||||
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
|
||||
|
||||
WHERE ((__cm.transferId = \\"50\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
|
||||
",
|
||||
false,
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler query expressions with field remapping 2`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(query),
|
||||
"
|
||||
SELECT id FROM transactions
|
||||
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
|
||||
LEFT JOIN categories t1 ON __cm.transferId = t1.id
|
||||
WHERE ((t1.name = \\"foo\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
|
||||
",
|
||||
false,
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler query expressions with field remapping 3`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(query),
|
||||
"
|
||||
SELECT id, t1.name FROM transactions
|
||||
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
|
||||
LEFT JOIN categories t1 ON __cm.transferId = t1.id
|
||||
WHERE ((__cm.transferId = \\"50\\")) AND transactions.isParent = 0 AND transactions.tombstone = 0
|
||||
",
|
||||
false,
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
|
||||
exports[`Compiler query expressions with null 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Symbol(query),
|
||||
"
|
||||
SELECT count(amount) FROM transactions
|
||||
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
|
||||
LEFT JOIN accounts t1 ON t1.id = transactions.acct
|
||||
WHERE (((t1.offbudget = 0) and (__cm.transferId IS NULL))) AND transactions.isParent = 0 AND transactions.tombstone = 0
|
||||
",
|
||||
true,
|
||||
],
|
||||
Array [
|
||||
Symbol(mov),
|
||||
Object {
|
||||
"index": 1,
|
||||
"type": "__reg",
|
||||
},
|
||||
Object {
|
||||
"name": "generated!result",
|
||||
"type": "__var",
|
||||
},
|
||||
],
|
||||
]
|
||||
`;
|
||||
@@ -1,50 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`lexer basic 1`] = `
|
||||
Array [
|
||||
Object {
|
||||
"colno": 0,
|
||||
"lineno": 0,
|
||||
"type": "whitespace",
|
||||
"value": "
|
||||
",
|
||||
},
|
||||
Object {
|
||||
"colno": 5,
|
||||
"lineno": 1,
|
||||
"type": "symbol",
|
||||
"value": "x",
|
||||
},
|
||||
Object {
|
||||
"colno": 6,
|
||||
"lineno": 1,
|
||||
"type": "whitespace",
|
||||
"value": " ",
|
||||
},
|
||||
Object {
|
||||
"colno": 7,
|
||||
"lineno": 1,
|
||||
"type": "operator",
|
||||
"value": "!=~",
|
||||
},
|
||||
Object {
|
||||
"colno": 10,
|
||||
"lineno": 1,
|
||||
"type": "whitespace",
|
||||
"value": " ",
|
||||
},
|
||||
Object {
|
||||
"colno": 11,
|
||||
"lineno": 1,
|
||||
"type": "int",
|
||||
"value": "4",
|
||||
},
|
||||
Object {
|
||||
"colno": 12,
|
||||
"lineno": 1,
|
||||
"type": "whitespace",
|
||||
"value": "
|
||||
",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -1,15 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`vm basic 1`] = `
|
||||
Object {
|
||||
"firstValue": [Function],
|
||||
"generated!result": -6,
|
||||
"number": [Function],
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`vm boolean types 1`] = `
|
||||
Object {
|
||||
"generated!result": 0,
|
||||
}
|
||||
`;
|
||||
@@ -1,95 +0,0 @@
|
||||
import { compile } from './compiler';
|
||||
|
||||
describe('Compiler', () => {
|
||||
test('get-query', () => {
|
||||
compile(
|
||||
'=from transactions where acct.offbudget = 0 and category = null and (description.transfer_acct.offbudget = 1 or description.transfer_acct = null) calculate { count(date) }',
|
||||
);
|
||||
});
|
||||
|
||||
test('basic', () => {
|
||||
let ops = compile(`
|
||||
=first(number(from transactions
|
||||
where
|
||||
date >= 20170101 and
|
||||
date <= 20170131 and
|
||||
acct.bank.name = 1
|
||||
calculate { sum(amount) }))
|
||||
`).ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
|
||||
ops = compile('').ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('parens', () => {
|
||||
let ops = compile('=(1 + 2)').ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
|
||||
ops = compile('=(1232 + 2) - (3 + 4)').ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('compiler binary ops', () => {
|
||||
let ops = compile('=foo + bar + baz + boo').ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('compiler nested funcs', () => {
|
||||
let ops = compile('=min(0, number(-20000))').ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('compiles boolean types', () => {
|
||||
let ops = compile('=if(true and 1) { 0 } else { 1 } ').ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('query expressions', () => {
|
||||
let ops = compile(`
|
||||
=from transactions
|
||||
where amount > 0
|
||||
select { sum(amount) as a }
|
||||
`).ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('query expressions with null', () => {
|
||||
let ops = compile(`
|
||||
=from transactions where acct.offbudget = 0 and category = null calculate { count(amount) }
|
||||
`).ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('complex query expressions', () => {
|
||||
let ops = compile(`
|
||||
=from transactions groupby substr(date, 0, 7) select { substr(date, 0, 7), sum(amount) }
|
||||
`).ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('query expressions with field remapping', () => {
|
||||
let ops = compile(`
|
||||
=from transactions where category = "50" select { id }
|
||||
`).ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
|
||||
ops = compile(`
|
||||
=from transactions where category.name = "foo" select { id }
|
||||
`).ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
|
||||
ops = compile(`
|
||||
=from transactions where category = "50" select { id, category.name }
|
||||
`).ops;
|
||||
expect(ops).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('field dependencies', () => {
|
||||
let sqlDependencies = compile(
|
||||
'=from transactions where acct.offbudget = 0 and category = null and (description.transfer_acct.offbudget = 1 or description.transfer_acct = null) calculate { count(date) }',
|
||||
).sqlDependencies;
|
||||
|
||||
expect(sqlDependencies[0].fields).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
import getSqlFields from './get-sql-fields';
|
||||
import * as nodes from './nodes';
|
||||
import {
|
||||
MOV,
|
||||
CALL,
|
||||
QUERY,
|
||||
UOP,
|
||||
BOP,
|
||||
REG1,
|
||||
SP,
|
||||
VAR,
|
||||
JUMPF,
|
||||
JUMPT,
|
||||
LABEL,
|
||||
} from './ops';
|
||||
import parse from './parser';
|
||||
import generateSql from './sqlgen';
|
||||
|
||||
class Compiler {
|
||||
src;
|
||||
scopeName;
|
||||
binding;
|
||||
ops;
|
||||
dependencies;
|
||||
sqlDependencies;
|
||||
|
||||
constructor() {
|
||||
this.ops = [];
|
||||
this.dependencies = [];
|
||||
this.sqlDependencies = [];
|
||||
}
|
||||
|
||||
fail(msg, lineno, colno) {
|
||||
const lines = this.src.split('\n');
|
||||
|
||||
let space = '';
|
||||
for (let i = 0; i < colno; i++) {
|
||||
space += ' ';
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`[${lineno + 1}, ${colno + 1}] ${msg}:\n${lines[lineno]}\n${space}^`,
|
||||
);
|
||||
}
|
||||
|
||||
resolveVariable(name) {
|
||||
if (name.indexOf('!') === -1) {
|
||||
return this.scopeName + '!' + name;
|
||||
}
|
||||
return name;
|
||||
}
|
||||
|
||||
maybePushStack(node, si) {
|
||||
if (node instanceof nodes.Symbol) {
|
||||
// There's no need to push anything to the stack since it's a
|
||||
// direct variable reference. Just store the referenced variable
|
||||
// and pop the symbol operation off the stack.
|
||||
const op = this.ops.pop();
|
||||
return [si, op[1]];
|
||||
}
|
||||
|
||||
this.ops.push([MOV, REG1, SP(si)]);
|
||||
return [si + 1, SP(si)];
|
||||
}
|
||||
|
||||
compileLiteral(node, si) {
|
||||
this.ops.push([MOV, node.value, REG1]);
|
||||
}
|
||||
|
||||
compileSymbol(node, si) {
|
||||
const resolved = this.resolveVariable(node.value);
|
||||
this.dependencies.push(resolved);
|
||||
this.ops.push([MOV, VAR(resolved), REG1]);
|
||||
}
|
||||
|
||||
compileBinOp(node, si) {
|
||||
this.compile(node.left, si);
|
||||
// TODO: Get rid of all this and add a second pass which optimizes
|
||||
// the opcodes.
|
||||
let left;
|
||||
[si, left] = this.maybePushStack(node.left, si);
|
||||
|
||||
this.compile(node.right, si + 1);
|
||||
this.ops.push([BOP, node.op, left, REG1]);
|
||||
}
|
||||
|
||||
compileUnaryOp(node, si) {
|
||||
this.compile(node.target, si);
|
||||
this.ops.push([UOP, node.op, REG1]);
|
||||
}
|
||||
|
||||
compileFunCall(node, si) {
|
||||
this.compile(node.callee, si);
|
||||
let callee;
|
||||
[si, callee] = this.maybePushStack(node.callee, si);
|
||||
|
||||
const args = node.args.children.map((arg, i) => {
|
||||
this.compile(arg, si + i);
|
||||
this.ops.push([MOV, REG1, SP(si + i)]);
|
||||
return SP(si + i);
|
||||
});
|
||||
|
||||
this.ops.push([CALL, callee, args]);
|
||||
}
|
||||
|
||||
compileQuery(node, si) {
|
||||
let fields = getSqlFields(node.table, node.where)
|
||||
.concat(getSqlFields(node.table, node.groupby))
|
||||
.concat(...node.select.map(s => getSqlFields(node.table, s.expr)));
|
||||
|
||||
const { sql, where } = generateSql(
|
||||
node.table,
|
||||
node.where,
|
||||
node.groupby,
|
||||
node.select,
|
||||
);
|
||||
|
||||
// TODO: This is a hack, but I'm pretty sure we can get rid of all
|
||||
// of this. Just need to think through it.
|
||||
fields = fields.map(f => (f === '__cm.transferId' ? 'category' : f));
|
||||
|
||||
// Uniquify them
|
||||
fields = [...new Set(fields)];
|
||||
|
||||
this.sqlDependencies.push({ table: node.table, where, fields });
|
||||
this.ops.push([QUERY, sql, node.calculated]);
|
||||
}
|
||||
|
||||
compileIf(node, si) {
|
||||
const L0 = LABEL();
|
||||
const L1 = LABEL();
|
||||
|
||||
this.compile(node.cond, si);
|
||||
this.ops.push([MOV, REG1, SP(si)]);
|
||||
|
||||
this.ops.push([JUMPF, SP(si), L0]);
|
||||
this.compile(node.body, si + 1);
|
||||
|
||||
this.ops.push([JUMPT, SP(si), L1]);
|
||||
L0.resolve(this.ops.length - 1);
|
||||
this.compile(node.else_, si + 1);
|
||||
L1.resolve(this.ops.length - 1);
|
||||
}
|
||||
|
||||
compileRoot(node, si) {
|
||||
node.children.forEach(node => {
|
||||
this.compile(node, si);
|
||||
});
|
||||
}
|
||||
|
||||
compile(node, si) {
|
||||
const method = this['compile' + node.getTypeName()];
|
||||
if (!method) {
|
||||
this.fail(
|
||||
'Unknown node type: ' + node.getTypeName(),
|
||||
node.lineno,
|
||||
node.colno,
|
||||
);
|
||||
}
|
||||
return method.call(this, node, si);
|
||||
}
|
||||
|
||||
compileSource(binding, scopeName, src) {
|
||||
this.src = src;
|
||||
this.scopeName = scopeName;
|
||||
this.binding = binding;
|
||||
|
||||
this.compile(parse(src), 0);
|
||||
|
||||
const resolvedBinding = this.resolveVariable(binding);
|
||||
|
||||
if (this.ops.length !== 0) {
|
||||
this.ops.push([MOV, REG1, VAR(resolvedBinding)]);
|
||||
} else {
|
||||
this.ops.push([MOV, '', VAR(resolvedBinding)]);
|
||||
}
|
||||
|
||||
return {
|
||||
ops: this.ops,
|
||||
dependencies: this.dependencies,
|
||||
sqlDependencies: this.sqlDependencies,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function compile(src) {
|
||||
return compileBinding('result', 'generated', src);
|
||||
}
|
||||
|
||||
export function compileBinding(binding, scopeName, src) {
|
||||
const compiler = new Compiler();
|
||||
return compiler.compileSource(binding, scopeName, src);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
function traverse(expr, fields) {
|
||||
switch (expr.getTypeName()) {
|
||||
case 'FunCall':
|
||||
expr.args.children.map(arg => traverse(arg, fields));
|
||||
break;
|
||||
|
||||
case 'Member':
|
||||
// Right now we only track dependencies on the top-level table,
|
||||
// and not any of the joined data. This tracks that field itself
|
||||
// that is joined on, but not the joined data yet.
|
||||
traverse(expr.object, fields);
|
||||
break;
|
||||
|
||||
case 'Literal':
|
||||
break;
|
||||
|
||||
case 'Symbol':
|
||||
if (fields.indexOf(expr.value) === -1 && expr.value !== 'null') {
|
||||
fields.push(expr.value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'BinOp':
|
||||
traverse(expr.left, fields);
|
||||
traverse(expr.right, fields);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unhandled node type: ' + expr.getTypeName());
|
||||
}
|
||||
}
|
||||
|
||||
export default function getSqlFields(table, ast) {
|
||||
let fields: string[] = [];
|
||||
if (!ast) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
traverse(ast, fields);
|
||||
|
||||
// These are implicit fields added by the sql generator. Going to
|
||||
// revisit how to track all of this.
|
||||
if (table === 'transactions') {
|
||||
fields.push('isParent');
|
||||
fields.push('tombstone');
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import lex from './lexer';
|
||||
|
||||
function getTokens(tokens) {
|
||||
const toks = [];
|
||||
while (!tokens.is_finished()) {
|
||||
toks.push(tokens.nextToken());
|
||||
}
|
||||
return toks;
|
||||
}
|
||||
|
||||
test('lexer basic', () => {
|
||||
const tokens = lex(`
|
||||
=x !=~ 4
|
||||
`);
|
||||
|
||||
expect(getTokens(tokens)).toMatchSnapshot();
|
||||
});
|
||||
@@ -1,310 +0,0 @@
|
||||
const whitespaceChars = new Set(' \n\t\r\u00A0');
|
||||
const delimChars = new Set('()[]{}%*-+~/#,:|.<>=!');
|
||||
const whitespaceAndDelimChars = new Set([...whitespaceChars, ...delimChars]);
|
||||
const intChars = new Set('0123456789');
|
||||
|
||||
const complexOps = new Set(['==', '!=', '<=', '>=', '=~', '!=~']);
|
||||
|
||||
export const TOKEN_STRING = 'string';
|
||||
export const TOKEN_WHITESPACE = 'whitespace';
|
||||
export const TOKEN_LEFT_PAREN = 'left-paren';
|
||||
export const TOKEN_RIGHT_PAREN = 'right-paren';
|
||||
export const TOKEN_LEFT_BRACKET = 'left-bracket';
|
||||
export const TOKEN_RIGHT_BRACKET = 'right-bracket';
|
||||
export const TOKEN_LEFT_CURLY = 'left-curly';
|
||||
export const TOKEN_RIGHT_CURLY = 'right-curly';
|
||||
export const TOKEN_COMMA = 'comma';
|
||||
export const TOKEN_INT = 'int';
|
||||
export const TOKEN_FLOAT = 'float';
|
||||
export const TOKEN_BOOLEAN = 'boolean';
|
||||
export const TOKEN_SYMBOL = 'symbol';
|
||||
export const TOKEN_DOT = 'dot';
|
||||
export const TOKEN_EXCLAIM = 'exclaim';
|
||||
export const TOKEN_OPERATOR = 'operator';
|
||||
|
||||
function token(type, value, lineno, colno) {
|
||||
return {
|
||||
type: type,
|
||||
value: value,
|
||||
lineno: lineno,
|
||||
colno: colno,
|
||||
};
|
||||
}
|
||||
|
||||
class Tokenizer {
|
||||
colno;
|
||||
hasCheckedMode;
|
||||
index;
|
||||
len;
|
||||
lineno;
|
||||
str;
|
||||
|
||||
constructor(str, opts = {}) {
|
||||
this.str = str;
|
||||
this.index = 0;
|
||||
this.len = str.length;
|
||||
this.lineno = 0;
|
||||
this.colno = 0;
|
||||
this.hasCheckedMode = false;
|
||||
}
|
||||
|
||||
nextToken() {
|
||||
let lineno = this.lineno;
|
||||
let colno = this.colno;
|
||||
let tok;
|
||||
let cur = this.current();
|
||||
|
||||
if (this.is_finished()) {
|
||||
return null;
|
||||
} else if ((tok = this._extract(whitespaceChars))) {
|
||||
// We hit some whitespace
|
||||
return token(TOKEN_WHITESPACE, tok, lineno, colno);
|
||||
} else if (!this.hasCheckedMode) {
|
||||
this.hasCheckedMode = true;
|
||||
|
||||
if (cur === '=') {
|
||||
this.forward();
|
||||
cur = this.current();
|
||||
return this.nextToken();
|
||||
} else {
|
||||
this.index = this.str.length;
|
||||
return token(TOKEN_STRING, this.str, lineno, colno);
|
||||
}
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
} else if (cur === '"' || cur === "'") {
|
||||
// We've hit a string
|
||||
return token(TOKEN_STRING, this.parseString(cur), lineno, colno);
|
||||
} else if (delimChars.has(cur)) {
|
||||
// We've hit a delimiter (a special char like a bracket)
|
||||
let type;
|
||||
|
||||
if (complexOps.has(cur + this.next() + this.next(2))) {
|
||||
cur = cur + this.next() + this.next(2);
|
||||
this.forward();
|
||||
this.forward();
|
||||
} else if (complexOps.has(cur + this.next())) {
|
||||
cur = cur + this.next();
|
||||
this.forward();
|
||||
}
|
||||
this.forward();
|
||||
|
||||
switch (cur) {
|
||||
case '(':
|
||||
type = TOKEN_LEFT_PAREN;
|
||||
break;
|
||||
case ')':
|
||||
type = TOKEN_RIGHT_PAREN;
|
||||
break;
|
||||
case '[':
|
||||
type = TOKEN_LEFT_BRACKET;
|
||||
break;
|
||||
case ']':
|
||||
type = TOKEN_RIGHT_BRACKET;
|
||||
break;
|
||||
case '{':
|
||||
type = TOKEN_LEFT_CURLY;
|
||||
break;
|
||||
case '}':
|
||||
type = TOKEN_RIGHT_CURLY;
|
||||
break;
|
||||
case ',':
|
||||
type = TOKEN_COMMA;
|
||||
break;
|
||||
case '.':
|
||||
type = TOKEN_DOT;
|
||||
break;
|
||||
case '!':
|
||||
type = TOKEN_EXCLAIM;
|
||||
break;
|
||||
default:
|
||||
type = TOKEN_OPERATOR;
|
||||
}
|
||||
|
||||
return token(type, cur, lineno, colno);
|
||||
} else {
|
||||
// We are not at whitespace or a delimiter, so extract the
|
||||
// text and parse it
|
||||
tok = this._extractUntil(whitespaceAndDelimChars);
|
||||
|
||||
if (tok.match(/^[-+]?[0-9]+$/)) {
|
||||
if (this.current() === '.') {
|
||||
this.forward();
|
||||
let dec = this._extract(intChars);
|
||||
return token(TOKEN_FLOAT, tok + '.' + dec, lineno, colno);
|
||||
} else {
|
||||
return token(TOKEN_INT, tok, lineno, colno);
|
||||
}
|
||||
} else if (tok.match(/^(true|false)$/)) {
|
||||
return token(TOKEN_BOOLEAN, tok, lineno, colno);
|
||||
} else if (tok.match(/^(or|and|not)$/)) {
|
||||
return token(TOKEN_OPERATOR, tok, lineno, colno);
|
||||
} else if (tok) {
|
||||
return token(TOKEN_SYMBOL, tok, lineno, colno);
|
||||
} else {
|
||||
throw new Error('Unexpected value while parsing: ' + tok);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
parseString(delimiter) {
|
||||
this.forward();
|
||||
|
||||
let str = '';
|
||||
|
||||
while (!this.is_finished() && this.current() !== delimiter) {
|
||||
let cur = this.current();
|
||||
|
||||
if (cur === '\\') {
|
||||
this.forward();
|
||||
switch (this.current()) {
|
||||
case 'n':
|
||||
str += '\n';
|
||||
break;
|
||||
case 't':
|
||||
str += '\t';
|
||||
break;
|
||||
case 'r':
|
||||
str += '\r';
|
||||
break;
|
||||
default:
|
||||
str += this.current();
|
||||
}
|
||||
this.forward();
|
||||
} else {
|
||||
str += cur;
|
||||
this.forward();
|
||||
}
|
||||
}
|
||||
|
||||
this.forward();
|
||||
return str;
|
||||
}
|
||||
|
||||
_matches(str) {
|
||||
if (this.index + str.length > this.len) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let m = this.str.slice(this.index, this.index + str.length);
|
||||
return m === str;
|
||||
}
|
||||
|
||||
_extractString(str) {
|
||||
if (this._matches(str)) {
|
||||
this.index += str.length;
|
||||
return str;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
_extractUntil(chars) {
|
||||
// Extract all non-matching chars, with the default matching set
|
||||
// to everything
|
||||
return this._extractMatching(true, chars || new Set());
|
||||
}
|
||||
|
||||
_extract(chars) {
|
||||
// Extract all matching chars (no default, so charString must be
|
||||
// explicit)
|
||||
return this._extractMatching(false, chars);
|
||||
}
|
||||
|
||||
_extractMatching(breakOnMatch, chars) {
|
||||
// Pull out characters until a breaking char is hit.
|
||||
// If breakOnMatch is false, a non-matching char stops it.
|
||||
// If breakOnMatch is true, a matching char stops it.
|
||||
|
||||
if (this.is_finished()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let matches = chars.has(this.current());
|
||||
|
||||
// Only proceed if the first character meets our condition
|
||||
if ((breakOnMatch && !matches) || (!breakOnMatch && matches)) {
|
||||
let t = this.current();
|
||||
this.forward();
|
||||
|
||||
// And pull out all the chars one at a time until we hit a
|
||||
// breaking char
|
||||
let isMatch = chars.has(this.current());
|
||||
|
||||
while (
|
||||
((breakOnMatch && !isMatch) || (!breakOnMatch && isMatch)) &&
|
||||
!this.is_finished()
|
||||
) {
|
||||
t += this.current();
|
||||
this.forward();
|
||||
|
||||
isMatch = chars.has(this.current());
|
||||
}
|
||||
|
||||
return t;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
is_finished() {
|
||||
return this.index >= this.len;
|
||||
}
|
||||
|
||||
forward() {
|
||||
this.index++;
|
||||
|
||||
if (this.previous() === '\n') {
|
||||
this.lineno++;
|
||||
this.colno = 0;
|
||||
} else {
|
||||
this.colno++;
|
||||
}
|
||||
}
|
||||
|
||||
back() {
|
||||
this.index--;
|
||||
|
||||
if (this.current() === '\n') {
|
||||
this.lineno--;
|
||||
|
||||
let idx = this.str.lastIndexOf('\n', this.index - 1);
|
||||
if (idx === -1) {
|
||||
this.colno = this.index;
|
||||
} else {
|
||||
this.colno = this.index - idx;
|
||||
}
|
||||
} else {
|
||||
this.colno--;
|
||||
}
|
||||
}
|
||||
|
||||
// current returns current character
|
||||
current() {
|
||||
if (!this.is_finished()) {
|
||||
return this.str.charAt(this.index);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
next(idx = 1) {
|
||||
if (this.index + idx < this.str.length) {
|
||||
return this.str.charAt(this.index + idx);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
// currentStr returns what's left of the unparsed string
|
||||
currentStr() {
|
||||
if (!this.is_finished()) {
|
||||
return this.str.substr(this.index);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
previous() {
|
||||
return this.str.charAt(this.index - 1);
|
||||
}
|
||||
}
|
||||
|
||||
export default function lex(src, opts?: Record<string, unknown>) {
|
||||
return new Tokenizer(src, opts);
|
||||
}
|
||||
@@ -1,201 +0,0 @@
|
||||
class Node {
|
||||
colno;
|
||||
fieldNames;
|
||||
lineno;
|
||||
|
||||
constructor(lineno, colno, fieldNames) {
|
||||
this.lineno = lineno;
|
||||
this.colno = colno;
|
||||
this.fieldNames = fieldNames;
|
||||
}
|
||||
|
||||
getTypeName() {
|
||||
return 'Node';
|
||||
}
|
||||
|
||||
traverseFields(onEnter, onExit) {
|
||||
const fieldNames = this.fieldNames;
|
||||
for (let i = 0; i < fieldNames.length; i++) {
|
||||
const val = this[fieldNames[i]];
|
||||
|
||||
if (val instanceof Node) {
|
||||
const ret = val.traverse(onEnter, onExit);
|
||||
if (ret) {
|
||||
this[fieldNames[i]] = ret;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
traverse(onEnter, onExit) {
|
||||
if (onEnter) {
|
||||
const val = onEnter(this);
|
||||
if (val === true) {
|
||||
return;
|
||||
} else if (val != null) {
|
||||
return val;
|
||||
}
|
||||
}
|
||||
this.traverseFields(onEnter, onExit);
|
||||
onExit && onExit(this);
|
||||
}
|
||||
|
||||
copy() {
|
||||
const inst = Object.assign(
|
||||
Object.create(Object.getPrototypeOf(this)),
|
||||
this,
|
||||
);
|
||||
|
||||
for (let i = 0; i < inst.fieldNames.length; i++) {
|
||||
const field = inst.fieldNames[i];
|
||||
if (inst[field] instanceof Node) {
|
||||
inst[field] = inst[field].copy();
|
||||
}
|
||||
}
|
||||
|
||||
return inst;
|
||||
}
|
||||
}
|
||||
|
||||
export class NodeList extends Node {
|
||||
children;
|
||||
|
||||
constructor(lineno, colno, nodes: unknown[] = []) {
|
||||
super(lineno, colno, ['children']);
|
||||
this.children = nodes;
|
||||
}
|
||||
|
||||
getTypeName() {
|
||||
return 'NodeList';
|
||||
}
|
||||
|
||||
addChild(node) {
|
||||
this.children.push(node);
|
||||
}
|
||||
|
||||
traverseFields(onEnter, onExit) {
|
||||
for (let i = 0; i < this.children.length; i++) {
|
||||
this.children[i].traverse(onEnter, onExit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class Root extends NodeList {
|
||||
getTypeName() {
|
||||
return 'Root';
|
||||
}
|
||||
}
|
||||
|
||||
export class Value extends Node {
|
||||
value;
|
||||
|
||||
constructor(lineno, colno, value) {
|
||||
super(lineno, colno, ['value']);
|
||||
this.value = value ?? null;
|
||||
}
|
||||
getTypeName() {
|
||||
return 'Value';
|
||||
}
|
||||
}
|
||||
export class UnaryOp extends Node {
|
||||
op;
|
||||
target;
|
||||
|
||||
constructor(lineno, colno, op, target) {
|
||||
super(lineno, colno, ['op', 'target']);
|
||||
this.op = op ?? null;
|
||||
this.target = target ?? null;
|
||||
}
|
||||
getTypeName() {
|
||||
return 'UnaryOp';
|
||||
}
|
||||
}
|
||||
export class BinOp extends Node {
|
||||
op;
|
||||
left;
|
||||
right;
|
||||
|
||||
constructor(lineno, colno, op, left, right) {
|
||||
super(lineno, colno, ['op', 'left', 'right']);
|
||||
this.op = op ?? null;
|
||||
this.left = left ?? null;
|
||||
this.right = right ?? null;
|
||||
}
|
||||
getTypeName() {
|
||||
return 'BinOp';
|
||||
}
|
||||
}
|
||||
|
||||
export class Literal extends Value {
|
||||
getTypeName() {
|
||||
return 'Literal';
|
||||
}
|
||||
}
|
||||
export class Symbol extends Value {
|
||||
getTypeName() {
|
||||
return 'Symbol';
|
||||
}
|
||||
}
|
||||
export class FunCall extends Node {
|
||||
callee;
|
||||
args;
|
||||
|
||||
constructor(lineno, colno, callee, args) {
|
||||
super(lineno, colno, ['callee', 'args']);
|
||||
this.callee = callee ?? null;
|
||||
this.args = args ?? null;
|
||||
}
|
||||
getTypeName() {
|
||||
return 'FunCall';
|
||||
}
|
||||
}
|
||||
|
||||
export class Member extends Node {
|
||||
object;
|
||||
property;
|
||||
|
||||
constructor(lineno, colno, object, property) {
|
||||
super(lineno, colno, ['object', 'property']);
|
||||
this.object = object ?? null;
|
||||
this.property = property ?? null;
|
||||
}
|
||||
getTypeName() {
|
||||
return 'Member';
|
||||
}
|
||||
}
|
||||
|
||||
export class Query extends Node {
|
||||
table;
|
||||
select;
|
||||
where;
|
||||
groupby;
|
||||
calculated;
|
||||
|
||||
constructor(lineno, colno, table, select, where, groupby, calculated) {
|
||||
super(lineno, colno, ['table', 'select', 'where', 'groupby', 'calculated']);
|
||||
this.table = table ?? null;
|
||||
this.select = select ?? null;
|
||||
this.where = where ?? null;
|
||||
this.groupby = groupby ?? null;
|
||||
this.calculated = calculated ?? null;
|
||||
}
|
||||
getTypeName() {
|
||||
return 'Query';
|
||||
}
|
||||
}
|
||||
|
||||
export class If extends Node {
|
||||
cond;
|
||||
body;
|
||||
else_;
|
||||
|
||||
constructor(lineno, colno, cond, body, else_) {
|
||||
super(lineno, colno, ['cond', 'body', 'else_']);
|
||||
this.cond = cond ?? null;
|
||||
this.body = body ?? null;
|
||||
this.else_ = else_ ?? null;
|
||||
}
|
||||
getTypeName() {
|
||||
return 'If';
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
export const MOV = Symbol('mov');
|
||||
export const CALL = Symbol('call');
|
||||
export const QUERY = Symbol('query');
|
||||
export const UOP = Symbol('uop');
|
||||
export const BOP = Symbol('bop');
|
||||
export const JUMPF = Symbol('jumpf');
|
||||
export const JUMPT = Symbol('jumpt');
|
||||
|
||||
export const REG1 = { type: '__reg', index: 1 };
|
||||
|
||||
export function SP(n) {
|
||||
return { type: '__stack', index: n };
|
||||
}
|
||||
|
||||
export function VAR(name) {
|
||||
return { type: '__var', name: name };
|
||||
}
|
||||
|
||||
export function LABEL() {
|
||||
let idx = null;
|
||||
return {
|
||||
get() {
|
||||
if (idx === null) {
|
||||
throw new Error('Attempted access of unresolved label');
|
||||
}
|
||||
return idx;
|
||||
},
|
||||
|
||||
resolve(n) {
|
||||
idx = n;
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -1,462 +0,0 @@
|
||||
import lex, * as types from './lexer';
|
||||
import * as nodes from './nodes';
|
||||
|
||||
function nextToken(state, withWhitespace?: boolean) {
|
||||
let tok;
|
||||
let { peeked, tokens } = state;
|
||||
|
||||
if (peeked) {
|
||||
if (!withWhitespace && peeked.type === types.TOKEN_WHITESPACE) {
|
||||
state.peeked = null;
|
||||
} else {
|
||||
tok = state.peeked;
|
||||
state.peeked = null;
|
||||
return tok;
|
||||
}
|
||||
}
|
||||
|
||||
tok = tokens.nextToken();
|
||||
|
||||
if (!withWhitespace) {
|
||||
while (tok && tok.type === types.TOKEN_WHITESPACE) {
|
||||
tok = tokens.nextToken();
|
||||
}
|
||||
}
|
||||
|
||||
return tok;
|
||||
}
|
||||
|
||||
function peekToken(state) {
|
||||
state.peeked = state.peeked || nextToken(state);
|
||||
return state.peeked;
|
||||
}
|
||||
|
||||
function pushToken(state, tok) {
|
||||
if (state.peeked) {
|
||||
throw new Error('pushToken: can only push one token on between reads');
|
||||
}
|
||||
state.peeked = tok;
|
||||
}
|
||||
|
||||
function fail(state, msg, lineno?: number, colno?: number) {
|
||||
if (!peekToken(state)) {
|
||||
throw new Error(msg + '\n\nSource:\n' + state.src + '\n');
|
||||
}
|
||||
if (lineno === undefined || colno === undefined) {
|
||||
const tok = peekToken(state);
|
||||
lineno = tok.lineno;
|
||||
colno = tok.colno;
|
||||
}
|
||||
|
||||
const lines = state.src.split('\n');
|
||||
|
||||
let space = '';
|
||||
for (let i = 0; i < colno; i++) {
|
||||
space += ' ';
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`[${lineno + 1}, ${colno + 1}] ${msg}:\n${lines[lineno]}\n${space}^`,
|
||||
);
|
||||
}
|
||||
|
||||
function skip(state, type) {
|
||||
let tok = nextToken(state);
|
||||
if (!tok || tok.type !== type) {
|
||||
pushToken(state, tok);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function expectValue(state, type, value) {
|
||||
let tok = nextToken(state);
|
||||
if (tok.type !== type || tok.value !== value) {
|
||||
fail(
|
||||
state,
|
||||
'expected ' + value + ', got ' + tok.value,
|
||||
tok.lineno,
|
||||
tok.colno,
|
||||
);
|
||||
}
|
||||
return tok;
|
||||
}
|
||||
|
||||
function expect(state, type) {
|
||||
let tok = nextToken(state);
|
||||
if (tok.type !== type) {
|
||||
fail(
|
||||
state,
|
||||
'expected ' + type + ', got ' + tok.type,
|
||||
tok.lineno,
|
||||
tok.colno,
|
||||
);
|
||||
}
|
||||
return tok;
|
||||
}
|
||||
|
||||
function skipValue(state, type, val) {
|
||||
let tok = nextToken(state);
|
||||
if (!tok || tok.type !== type || tok.value !== val) {
|
||||
pushToken(state, tok);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function skipSymbol(state, val) {
|
||||
return skipValue(state, types.TOKEN_SYMBOL, val);
|
||||
}
|
||||
|
||||
function parseExpression(state) {
|
||||
return parseOr(state);
|
||||
}
|
||||
|
||||
function parseOr(state) {
|
||||
let left = parseAnd(state);
|
||||
while (skipValue(state, types.TOKEN_OPERATOR, 'or')) {
|
||||
const right = parseAnd(state);
|
||||
left = new nodes.BinOp(left.lineno, left.colno, 'or', left, right);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseAnd(state) {
|
||||
let left = parseNot(state);
|
||||
while (skipValue(state, types.TOKEN_OPERATOR, 'and')) {
|
||||
const right = parseNot(state);
|
||||
left = new nodes.BinOp(left.lineno, left.colno, 'and', left, right);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseNot(state) {
|
||||
let left = parseCompare(state);
|
||||
while (skipValue(state, types.TOKEN_OPERATOR, 'not')) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const right = parseCompare(state);
|
||||
left = new nodes.UnaryOp(left.lineno, left.colno, 'not', parseNot(state));
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseCompare(state) {
|
||||
let compareOps = ['=', '!=', '<', '>', '<=', '>=', '=~', '!=~'];
|
||||
let node = parseAdd(state);
|
||||
|
||||
while (1) {
|
||||
let tok = nextToken(state);
|
||||
|
||||
if (!tok) {
|
||||
break;
|
||||
} else if (compareOps.indexOf(tok.value) !== -1) {
|
||||
node = new nodes.BinOp(
|
||||
tok.lineno,
|
||||
tok.colno,
|
||||
tok.value,
|
||||
node,
|
||||
parseAdd(state),
|
||||
);
|
||||
} else {
|
||||
pushToken(state, tok);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function parseAdd(state) {
|
||||
let left = parseSub(state);
|
||||
while (skipValue(state, types.TOKEN_OPERATOR, '+')) {
|
||||
const right = parseSub(state);
|
||||
left = new nodes.BinOp(left.lineno, left.colno, '+', left, right);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseSub(state) {
|
||||
let left = parseMul(state);
|
||||
while (skipValue(state, types.TOKEN_OPERATOR, '-')) {
|
||||
const right = parseMul(state);
|
||||
left = new nodes.BinOp(left.lineno, left.colno, '-', left, right);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseMul(state) {
|
||||
let left = parseDiv(state);
|
||||
while (skipValue(state, types.TOKEN_OPERATOR, '*')) {
|
||||
const right = parseDiv(state);
|
||||
left = new nodes.BinOp(left.lineno, left.colno, '*', left, right);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseDiv(state) {
|
||||
let left = parseUnary(state);
|
||||
while (skipValue(state, types.TOKEN_OPERATOR, '/')) {
|
||||
const right = parseUnary(state);
|
||||
left = new nodes.BinOp(left.lineno, left.colno, '/', left, right);
|
||||
}
|
||||
return left;
|
||||
}
|
||||
|
||||
function parseUnary(state) {
|
||||
let tok = peekToken(state);
|
||||
|
||||
if (skipValue(state, types.TOKEN_OPERATOR, '-')) {
|
||||
const nextTok = peekToken(state);
|
||||
if (nextTok.type === types.TOKEN_INT) {
|
||||
const number = parseInt(nextToken(state).value);
|
||||
return new nodes.Literal(tok.lineno, tok.colno, -number);
|
||||
} else if (nextTok.type === types.TOKEN_FLOAT) {
|
||||
const number = parseFloat(nextToken(state).value);
|
||||
return new nodes.Literal(tok.lineno, tok.colno, -number);
|
||||
}
|
||||
|
||||
return new nodes.UnaryOp(tok.lineno, tok.colno, '-', parseUnary(state));
|
||||
}
|
||||
return parsePrimary(state);
|
||||
}
|
||||
|
||||
function parsePrimary(state) {
|
||||
let tok = nextToken(state);
|
||||
let val: number | boolean | null = null;
|
||||
|
||||
if (!tok) {
|
||||
fail(state, 'expected expression, got end of file');
|
||||
} else if (tok.type === types.TOKEN_STRING) {
|
||||
val = tok.value;
|
||||
} else if (tok.type === types.TOKEN_INT) {
|
||||
val = parseInt(tok.value, 10);
|
||||
} else if (tok.type === types.TOKEN_FLOAT) {
|
||||
val = parseFloat(tok.value);
|
||||
} else if (tok.type === types.TOKEN_BOOLEAN) {
|
||||
if (tok.value === 'true') {
|
||||
val = true;
|
||||
} else if (tok.value === 'false') {
|
||||
val = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (val !== null) {
|
||||
return new nodes.Literal(tok.lineno, tok.colno, val);
|
||||
} else if (tok.type === types.TOKEN_SYMBOL) {
|
||||
if (tok.value === 'from') {
|
||||
return parseQueryExpression(state);
|
||||
} else if (tok.value === 'if') {
|
||||
return parseIfExpression(state);
|
||||
}
|
||||
|
||||
return parsePostfix(
|
||||
state,
|
||||
new nodes.Symbol(tok.lineno, tok.colno, tok.value),
|
||||
);
|
||||
} else if (tok.type === types.TOKEN_LEFT_PAREN) {
|
||||
const node = parseExpression(state);
|
||||
expect(state, types.TOKEN_RIGHT_PAREN);
|
||||
return node;
|
||||
}
|
||||
|
||||
fail(state, 'Unexpected token: ' + tok.value, tok.lineno, tok.colno);
|
||||
}
|
||||
|
||||
function parseIfExpression(state) {
|
||||
const tok = expect(state, types.TOKEN_LEFT_PAREN);
|
||||
const cond = parseExpression(state);
|
||||
expect(state, types.TOKEN_RIGHT_PAREN);
|
||||
|
||||
expect(state, types.TOKEN_LEFT_CURLY);
|
||||
const body = parseExpression(state);
|
||||
expect(state, types.TOKEN_RIGHT_CURLY);
|
||||
|
||||
let else_;
|
||||
if (skipSymbol(state, 'else')) {
|
||||
expect(state, types.TOKEN_LEFT_CURLY);
|
||||
else_ = parseExpression(state);
|
||||
expect(state, types.TOKEN_RIGHT_CURLY);
|
||||
}
|
||||
|
||||
return new nodes.If(tok.lineno, tok.colno, cond, body, else_);
|
||||
}
|
||||
|
||||
function parseQueryExpression(state) {
|
||||
// The `from` keyword has already been parsed
|
||||
const tok = expect(state, types.TOKEN_SYMBOL);
|
||||
const table = tok.value;
|
||||
|
||||
let where = null;
|
||||
if (skipSymbol(state, 'where')) {
|
||||
where = parseQuerySubExpression(state);
|
||||
}
|
||||
|
||||
let groupby = null;
|
||||
if (skipSymbol(state, 'groupby')) {
|
||||
groupby = parseQuerySubExpression(state);
|
||||
}
|
||||
|
||||
let select: Array<{ expr: unknown; as?: unknown }> = [];
|
||||
let calculated;
|
||||
|
||||
if (skipSymbol(state, 'select')) {
|
||||
let checkComma = false;
|
||||
calculated = false;
|
||||
|
||||
expectValue(state, types.TOKEN_LEFT_CURLY, '{');
|
||||
|
||||
while (!skipValue(state, types.TOKEN_RIGHT_CURLY, '}')) {
|
||||
const tok = peekToken(state);
|
||||
|
||||
if (checkComma && !skip(state, types.TOKEN_COMMA)) {
|
||||
fail(
|
||||
state,
|
||||
'Unexpected token in query select: ' + tok.value,
|
||||
tok.lineno,
|
||||
tok.colno,
|
||||
);
|
||||
}
|
||||
|
||||
const expr = parseQuerySubExpression(state);
|
||||
let as = null;
|
||||
|
||||
if (skipSymbol(state, 'as')) {
|
||||
const tok = expect(state, types.TOKEN_SYMBOL);
|
||||
as = tok.value;
|
||||
}
|
||||
|
||||
select.push({ expr, as });
|
||||
|
||||
checkComma = true;
|
||||
}
|
||||
} else if (skipSymbol(state, 'calculate')) {
|
||||
calculated = true;
|
||||
|
||||
expectValue(state, types.TOKEN_LEFT_CURLY, '{');
|
||||
select.push({ expr: parseQuerySubExpression(state) });
|
||||
|
||||
if (!skipValue(state, types.TOKEN_RIGHT_CURLY, '}')) {
|
||||
fail(state, 'Only one expression allowed for `calculate`');
|
||||
}
|
||||
} else {
|
||||
fail(state, 'Expected either the `select` or `calculate` keyword');
|
||||
}
|
||||
|
||||
return new nodes.Query(
|
||||
tok.lineno,
|
||||
tok.colno,
|
||||
table,
|
||||
select,
|
||||
where,
|
||||
groupby,
|
||||
calculated,
|
||||
);
|
||||
}
|
||||
|
||||
function parseQuerySubExpression(state) {
|
||||
const node = parseExpression(state);
|
||||
return node;
|
||||
}
|
||||
|
||||
function parsePostfix(state, node) {
|
||||
let tok;
|
||||
|
||||
while ((tok = nextToken(state))) {
|
||||
if (tok.type === types.TOKEN_LEFT_PAREN) {
|
||||
pushToken(state, tok);
|
||||
let args = parseArgs(state);
|
||||
node = new nodes.FunCall(tok.lineno, tok.colno, node, args);
|
||||
} else if (tok.type === types.TOKEN_DOT) {
|
||||
const val = nextToken(state);
|
||||
node = new nodes.Member(
|
||||
tok.lineno,
|
||||
tok.colno,
|
||||
node,
|
||||
new nodes.Literal(val.lineno, val.colno, val.value),
|
||||
);
|
||||
} else if (tok.type === types.TOKEN_EXCLAIM) {
|
||||
const name = nextToken(state);
|
||||
if (name.type !== types.TOKEN_SYMBOL) {
|
||||
fail(
|
||||
state,
|
||||
'Expected cell name in sheet reference',
|
||||
name.lineno,
|
||||
name.colno,
|
||||
);
|
||||
}
|
||||
|
||||
return new nodes.Symbol(
|
||||
node.lineno,
|
||||
node.colno,
|
||||
node.value + '!' + name.value,
|
||||
);
|
||||
} else {
|
||||
pushToken(state, tok);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
function parseArgs(state) {
|
||||
let tok = peekToken(state);
|
||||
|
||||
if (tok.type !== types.TOKEN_LEFT_PAREN) {
|
||||
fail(state, 'Expected arguments', tok.lineno, tok.colno);
|
||||
}
|
||||
|
||||
nextToken(state);
|
||||
|
||||
let args = new nodes.NodeList(tok.lineno, tok.colno);
|
||||
let checkComma = false;
|
||||
|
||||
while (1) {
|
||||
tok = peekToken(state);
|
||||
if (tok.type === types.TOKEN_RIGHT_PAREN) {
|
||||
nextToken(state);
|
||||
break;
|
||||
}
|
||||
|
||||
if (checkComma && !skip(state, types.TOKEN_COMMA)) {
|
||||
fail(
|
||||
state,
|
||||
'Expected comma after function argument',
|
||||
tok.lineno,
|
||||
tok.colno,
|
||||
);
|
||||
}
|
||||
|
||||
args.addChild(parseExpression(state));
|
||||
checkComma = true;
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
export default function parse(src) {
|
||||
let state = {
|
||||
src: src,
|
||||
tokens: lex(src),
|
||||
peeked: null,
|
||||
};
|
||||
|
||||
if (state.tokens.is_finished()) {
|
||||
// If it's an empty string, return nothing
|
||||
return new nodes.Root(0, 0, []);
|
||||
} else {
|
||||
const expr = parseExpression(state);
|
||||
|
||||
const tok = nextToken(state);
|
||||
if (tok) {
|
||||
fail(
|
||||
state,
|
||||
'Unexpected token after expression: ' + tok.value,
|
||||
tok.lineno,
|
||||
tok.colno,
|
||||
);
|
||||
}
|
||||
|
||||
return new nodes.Root(0, 0, [expr]);
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
import { SCHEMA_PATHS } from './sqlgen';
|
||||
|
||||
export default function convert(table, item) {
|
||||
if (SCHEMA_PATHS[table]) {
|
||||
let fields = SCHEMA_PATHS[table];
|
||||
let updates = {};
|
||||
Object.keys(item).forEach(k => {
|
||||
let mappedField = fields[k] && fields[k].field;
|
||||
|
||||
if (mappedField) {
|
||||
updates[k] = item[mappedField];
|
||||
}
|
||||
});
|
||||
|
||||
return { ...item, ...updates };
|
||||
}
|
||||
return item;
|
||||
}
|
||||
@@ -1,303 +0,0 @@
|
||||
import * as nodes from './nodes';
|
||||
|
||||
type Lookup = { field: string; tableId?: string };
|
||||
|
||||
let _uid = 0;
|
||||
function resetUid() {
|
||||
_uid = 0;
|
||||
}
|
||||
|
||||
function uid() {
|
||||
_uid++;
|
||||
return 't' + _uid;
|
||||
}
|
||||
|
||||
function fail(node, message) {
|
||||
const err = new Error(message);
|
||||
// @ts-expect-error We should use error.cause to pass node info
|
||||
err.node = node;
|
||||
throw err;
|
||||
}
|
||||
|
||||
function generateExpression(expr) {
|
||||
if (typeof expr === 'string') {
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
return `"${expr}"`;
|
||||
} else if (typeof expr === 'number') {
|
||||
return expr;
|
||||
}
|
||||
|
||||
switch (expr.getTypeName()) {
|
||||
case 'FunCall':
|
||||
return (
|
||||
generateExpression(expr.callee) +
|
||||
'(' +
|
||||
expr.args.children.map(node => generateExpression(node)).join(',') +
|
||||
')'
|
||||
);
|
||||
case 'Member':
|
||||
return (
|
||||
generateExpression(expr.object) +
|
||||
'.' +
|
||||
generateExpression(expr.property)
|
||||
);
|
||||
case 'BinOp':
|
||||
const left = generateExpression(expr.left);
|
||||
let str;
|
||||
|
||||
if (
|
||||
expr.op === '=' &&
|
||||
expr.right.getTypeName() === 'Symbol' &&
|
||||
expr.right.value === 'null'
|
||||
) {
|
||||
str = left + ' IS NULL';
|
||||
} else {
|
||||
const right = generateExpression(expr.right);
|
||||
|
||||
switch (expr.op) {
|
||||
case '=~':
|
||||
str = `${left} LIKE ${right}`;
|
||||
break;
|
||||
case '!=~':
|
||||
str = `${left} NOT LIKE ${right}`;
|
||||
break;
|
||||
default:
|
||||
str = `${left} ${expr.op} ${right}`;
|
||||
}
|
||||
}
|
||||
|
||||
return '(' + str + ')';
|
||||
case 'Literal':
|
||||
if (typeof expr.value === 'string') {
|
||||
// eslint-disable-next-line rulesdir/typography
|
||||
return `"${expr.value}"`;
|
||||
}
|
||||
return expr.value;
|
||||
case 'Symbol':
|
||||
// if (expr.value.indexOf('!') !== -1) {
|
||||
// fail(expr, 'SQL variable cannot contain cell lookup');
|
||||
// }
|
||||
return expr.value;
|
||||
default:
|
||||
throw new Error('Unknown query node: ' + expr.getTypeName());
|
||||
}
|
||||
}
|
||||
|
||||
function transformColumns(node, implicitTable) {
|
||||
let transformed = node.traverse(n => {
|
||||
if (n instanceof nodes.Symbol) {
|
||||
let table = implicitTable;
|
||||
let field = n.value;
|
||||
|
||||
if (SCHEMA_PATHS[table] && SCHEMA_PATHS[table][field]) {
|
||||
let info = SCHEMA_PATHS[table][field];
|
||||
if (info.field) {
|
||||
// Map the field onto something else
|
||||
return new nodes.Symbol(n.lineno, n.colno, info.field);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return transformed || node;
|
||||
}
|
||||
|
||||
function transformLookups(node, implicitTable) {
|
||||
let paths: Lookup[][] = [];
|
||||
|
||||
const transformed = node.traverse(n => {
|
||||
if (n instanceof nodes.Member) {
|
||||
let currentNode = n;
|
||||
|
||||
let lookups: Lookup[] = [];
|
||||
while (currentNode instanceof nodes.Member) {
|
||||
if (!(currentNode.property instanceof nodes.Value)) {
|
||||
fail(currentNode, 'Invalid syntax for SQL reference');
|
||||
}
|
||||
|
||||
lookups.push({ field: currentNode.property.value });
|
||||
currentNode = currentNode.object;
|
||||
}
|
||||
|
||||
// @ts-expect-error Node refinement missing
|
||||
if (!(currentNode instanceof nodes.Symbol)) {
|
||||
fail(currentNode, 'Invalid syntax for SQL reference');
|
||||
}
|
||||
// @ts-expect-error Node refinement missing
|
||||
lookups.push({ field: currentNode.value });
|
||||
lookups.reverse();
|
||||
|
||||
lookups = lookups.map((lookup, idx) => {
|
||||
return {
|
||||
field: lookup.field,
|
||||
tableId: uid(),
|
||||
};
|
||||
});
|
||||
|
||||
let table = implicitTable;
|
||||
|
||||
// Skip the last field as we don't want to resolve to that
|
||||
// table. The syntax to emit is `table.field`.
|
||||
for (let i = 0; i < lookups.length - 1; i++) {
|
||||
const lookup = lookups[i];
|
||||
|
||||
if (!SCHEMA_PATHS[table]) {
|
||||
const err = new Error(
|
||||
`Table “${table}” not joinable for field “${lookup}”`,
|
||||
);
|
||||
// @ts-expect-error We should use error.cause to pass node info
|
||||
err.node = node;
|
||||
throw err;
|
||||
}
|
||||
if (!SCHEMA_PATHS[table][lookup.field]) {
|
||||
const err = new Error(
|
||||
`Unknown field “${lookup}” on table “${table}”`,
|
||||
);
|
||||
// @ts-expect-error We should use error.cause to pass node info
|
||||
err.node = node;
|
||||
throw err;
|
||||
}
|
||||
|
||||
table = SCHEMA_PATHS[table][lookup.field].table;
|
||||
}
|
||||
|
||||
paths.push(lookups);
|
||||
|
||||
let tableId = lookups[lookups.length - 2].tableId;
|
||||
let field = lookups[lookups.length - 1].field;
|
||||
|
||||
return new nodes.Member(
|
||||
node.lineno,
|
||||
node.colno,
|
||||
new nodes.Symbol(node.lineno, node.colno, tableId),
|
||||
new nodes.Symbol(node.lineno, node.colno, field),
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return { paths, node: transformed || node };
|
||||
}
|
||||
|
||||
export default function generate(table, where, groupby, select) {
|
||||
// Figure out the dep tables here. Return the SQL and dependent
|
||||
// tables
|
||||
let allPaths: Lookup[][] = [];
|
||||
|
||||
resetUid();
|
||||
|
||||
if (!tables[table]) {
|
||||
throw new Error('Table not found: ' + table);
|
||||
}
|
||||
|
||||
const selectStr = select
|
||||
.map(s => {
|
||||
let { paths, node } = transformLookups(s.expr, table);
|
||||
let as = s.as;
|
||||
allPaths = allPaths.concat(paths);
|
||||
|
||||
let newNode = transformColumns(node, table);
|
||||
|
||||
// If the selected field was transformed, select it as the
|
||||
// original name
|
||||
if (node !== newNode && node instanceof nodes.Symbol && !as) {
|
||||
as = node.value;
|
||||
}
|
||||
|
||||
const exprStr = generateExpression(newNode);
|
||||
return as ? `${exprStr} as ${as}` : exprStr;
|
||||
})
|
||||
.join(', ');
|
||||
|
||||
let whereStr = '';
|
||||
let whereTransformed;
|
||||
if (where) {
|
||||
let { paths, node } = transformLookups(where, table);
|
||||
allPaths = allPaths.concat(paths);
|
||||
whereTransformed = node.copy();
|
||||
|
||||
// Where clauses provide a special hook to map a column onto
|
||||
// something different, so you can represent something more
|
||||
// complex internally. You are still required to provide the
|
||||
// original name somehow; all other references use the original
|
||||
// name, so make sure you do `JOIN table <original-name>` or
|
||||
// something like that.
|
||||
node = transformColumns(node, table);
|
||||
|
||||
whereStr = ' WHERE (' + generateExpression(node) + ')';
|
||||
}
|
||||
|
||||
let groupByStr = '';
|
||||
if (groupby) {
|
||||
let { paths, node } = transformLookups(groupby, table);
|
||||
allPaths = allPaths.concat(paths);
|
||||
groupByStr = ' GROUP BY ' + generateExpression(node);
|
||||
}
|
||||
|
||||
let dependencies: string[] = [];
|
||||
let joins: string[] = [];
|
||||
|
||||
allPaths.forEach(path => {
|
||||
let currentTable = { name: table, id: table };
|
||||
for (let i = 0; i < path.length - 1; i++) {
|
||||
let lookup = path[i];
|
||||
let meta = SCHEMA_PATHS[currentTable.name][lookup.field];
|
||||
|
||||
if (meta.sql) {
|
||||
joins.push(meta.sql(lookup.tableId));
|
||||
} else {
|
||||
joins.push(
|
||||
`LEFT JOIN ${meta.table} ${lookup.tableId} ON ${lookup.tableId}.id = ${currentTable.id}.${lookup.field}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (dependencies.indexOf(meta.table) === -1) {
|
||||
dependencies.push(meta.table);
|
||||
}
|
||||
|
||||
currentTable = { name: meta.table, id: lookup.tableId };
|
||||
}
|
||||
});
|
||||
|
||||
const sql =
|
||||
tables[table](selectStr, whereStr, joins.join('\n')) + ' ' + groupByStr;
|
||||
|
||||
return {
|
||||
sql,
|
||||
where: whereTransformed,
|
||||
dependencies,
|
||||
};
|
||||
}
|
||||
|
||||
export const SCHEMA_PATHS = {
|
||||
transactions: {
|
||||
category: {
|
||||
table: 'categories',
|
||||
sql: id => `LEFT JOIN categories ${id} ON __cm.transferId = ${id}.id`,
|
||||
field: '__cm.transferId',
|
||||
},
|
||||
acct: { table: 'accounts' },
|
||||
description: { table: 'payees' },
|
||||
},
|
||||
payees: {
|
||||
transfer_acct: { table: 'accounts' },
|
||||
},
|
||||
accounts: {
|
||||
bank: { table: 'banks' },
|
||||
},
|
||||
};
|
||||
|
||||
const tables = {
|
||||
transactions: (select, where, join) => {
|
||||
// Never take into account parent split transactions. Their
|
||||
// children should sum up to be equal to it
|
||||
// prettier-ignore
|
||||
let whereStr = `${where === '' ? 'WHERE' : where + ' AND'} transactions.isParent = 0 AND transactions.tombstone = 0`;
|
||||
|
||||
return `
|
||||
SELECT ${select} FROM transactions
|
||||
LEFT JOIN category_mapping __cm ON __cm.id = transactions.category
|
||||
${join}
|
||||
${whereStr}
|
||||
`;
|
||||
},
|
||||
};
|
||||
@@ -1,58 +0,0 @@
|
||||
import { unresolveName } from '../util';
|
||||
|
||||
import VM from './vm';
|
||||
|
||||
const db = {
|
||||
runQuery: sql => {
|
||||
return Promise.resolve([{ 'sum(t.amount)': 1000 }]);
|
||||
},
|
||||
};
|
||||
|
||||
function makeScopes(vars) {
|
||||
return {
|
||||
getVariable: resolvedName => {
|
||||
const { name } = unresolveName(resolvedName);
|
||||
|
||||
if (vars[resolvedName] !== undefined) {
|
||||
return vars[resolvedName];
|
||||
} else if (vars[name] !== undefined) {
|
||||
return vars[name];
|
||||
}
|
||||
|
||||
throw new Error(`“${resolvedName}” is not defined`);
|
||||
},
|
||||
|
||||
setVariable: (name, value) => {
|
||||
vars[name] = value;
|
||||
},
|
||||
|
||||
getAll: () => vars,
|
||||
};
|
||||
}
|
||||
|
||||
function run(src, vars = {}) {
|
||||
const scopes = makeScopes(vars);
|
||||
const vm = new VM(db, scopes);
|
||||
|
||||
return new Promise(resolve => {
|
||||
vm.runSource(src, () => {
|
||||
expect(scopes.getAll()).toMatchSnapshot();
|
||||
resolve(undefined);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
test('vm basic', async () => {
|
||||
return run(`=-(1 + 2 + 3)`, {
|
||||
number: x => {
|
||||
return x;
|
||||
},
|
||||
firstValue: arr => {
|
||||
return arr[0]['sum(t.amount)'];
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('vm boolean types', async () => {
|
||||
return run('=if(true and (1 + 2 + 3 - 5)) { 0 } else { 1 } ');
|
||||
});
|
||||
@@ -1,193 +0,0 @@
|
||||
import { compile } from './compiler';
|
||||
import { MOV, CALL, QUERY, UOP, BOP, JUMPF, JUMPT } from './ops';
|
||||
|
||||
export default class VM {
|
||||
_onFinish;
|
||||
db;
|
||||
ops;
|
||||
paused;
|
||||
pc;
|
||||
reg1;
|
||||
scopes;
|
||||
stack;
|
||||
|
||||
constructor(db, scopes) {
|
||||
this.stack = new Array(1000);
|
||||
this.reg1 = null;
|
||||
this.pc = 0;
|
||||
this.db = db;
|
||||
this.scopes = scopes;
|
||||
}
|
||||
|
||||
get(ref) {
|
||||
if (ref && ref.type) {
|
||||
if (ref.type === '__reg') {
|
||||
return this.reg1;
|
||||
} else if (ref.type === '__var') {
|
||||
return this.scopes.getVariable(ref.name);
|
||||
} else if (ref.type === '__stack') {
|
||||
return this.stack[ref.index];
|
||||
}
|
||||
}
|
||||
|
||||
return ref;
|
||||
}
|
||||
|
||||
set(ref, value) {
|
||||
if (ref && ref.type) {
|
||||
if (ref.type === '__reg') {
|
||||
this.reg1 = this.get(value);
|
||||
} else if (ref.type === '__var') {
|
||||
this.scopes.setVariable(ref.name, this.get(value));
|
||||
} else if (ref.type === '__stack') {
|
||||
this.stack[ref.index] = this.get(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
binaryOp(op, left, right) {
|
||||
switch (op) {
|
||||
case '+':
|
||||
// TODO: Enforce these to be numbers
|
||||
this.reg1 = this.get(left) + this.get(right);
|
||||
break;
|
||||
case '-':
|
||||
this.reg1 = this.get(left) - this.get(right);
|
||||
break;
|
||||
case '*':
|
||||
this.reg1 = this.get(left) * this.get(right);
|
||||
break;
|
||||
case '/':
|
||||
this.reg1 = this.get(left) / this.get(right);
|
||||
break;
|
||||
case 'and':
|
||||
this.reg1 = this.get(left) && this.get(right);
|
||||
break;
|
||||
case 'or':
|
||||
this.reg1 = this.get(left) || this.get(right);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unimplemented operator: ' + op);
|
||||
}
|
||||
}
|
||||
|
||||
unaryOp(op, target) {
|
||||
switch (op) {
|
||||
case '-':
|
||||
this.reg1 = -this.get(target);
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unimplemented operator: ' + op);
|
||||
}
|
||||
}
|
||||
|
||||
call(callee, args) {
|
||||
const func = this.get(callee);
|
||||
this.reg1 = func.apply(
|
||||
null,
|
||||
args.map(arg => this.get(arg)),
|
||||
);
|
||||
}
|
||||
|
||||
query(sql, calculated) {
|
||||
this.pause(
|
||||
this.db.runQuery(sql, [], true).then(res => {
|
||||
if (calculated) {
|
||||
const keys = Object.keys(res[0]);
|
||||
return res[0][keys[0]];
|
||||
}
|
||||
return res;
|
||||
}),
|
||||
'Running sql: ' + sql,
|
||||
);
|
||||
}
|
||||
|
||||
jump(value, loc, { test }) {
|
||||
const result = this.get(value);
|
||||
const falsy = result === false || result === 0 || result === '';
|
||||
|
||||
if ((test === 'true' && !falsy) || (test === 'false' && falsy)) {
|
||||
this.pc = loc.get();
|
||||
}
|
||||
}
|
||||
|
||||
pause(promise, activityName) {
|
||||
this.paused = true;
|
||||
|
||||
promise.then(
|
||||
val => {
|
||||
this.resume(val);
|
||||
},
|
||||
err => {
|
||||
console.log('VM caught error during activity: ' + activityName);
|
||||
console.log(err);
|
||||
this.resume(null);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
resume(val) {
|
||||
this.reg1 = val;
|
||||
this.paused = false;
|
||||
this._run();
|
||||
}
|
||||
|
||||
_run() {
|
||||
while (this.pc < this.ops.length) {
|
||||
const op = this.ops[this.pc];
|
||||
|
||||
switch (op[0]) {
|
||||
case MOV:
|
||||
this.set(op[2], op[1]);
|
||||
break;
|
||||
case CALL:
|
||||
this.call(op[1], op[2]);
|
||||
break;
|
||||
case QUERY:
|
||||
this.query(op[1], op[2]);
|
||||
break;
|
||||
case BOP:
|
||||
this.binaryOp(op[1], op[2], op[3]);
|
||||
break;
|
||||
case UOP:
|
||||
this.unaryOp(op[1], op[2]);
|
||||
break;
|
||||
case JUMPF:
|
||||
this.jump(op[1], op[2], { test: 'false' });
|
||||
break;
|
||||
case JUMPT:
|
||||
this.jump(op[1], op[2], { test: 'true' });
|
||||
break;
|
||||
default:
|
||||
throw new Error('Unimplemented opcode: ' + op[0].toString());
|
||||
}
|
||||
|
||||
this.pc++;
|
||||
|
||||
if (this.paused) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (this.pc === this.ops.length && this._onFinish) {
|
||||
this._onFinish(this.reg1);
|
||||
}
|
||||
}
|
||||
|
||||
onFinish(func) {
|
||||
this._onFinish = func;
|
||||
}
|
||||
|
||||
run(ops, onFinish) {
|
||||
this.pc = 0;
|
||||
this.ops = ops;
|
||||
this._onFinish = onFinish;
|
||||
this._run();
|
||||
return this.reg1;
|
||||
}
|
||||
|
||||
runSource(src, onFinish) {
|
||||
const { ops } = compile(src);
|
||||
return this.run(ops, onFinish);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { compile } from './new/compiler';
|
||||
import sqlinterp from './sqlinterp';
|
||||
|
||||
test('sql interpretation works', async () => {
|
||||
const transJan = {
|
||||
date: 20170106,
|
||||
amount: -5000,
|
||||
acct: 'boa',
|
||||
category: 1,
|
||||
};
|
||||
const transFeb = {
|
||||
date: 20170215,
|
||||
amount: -7620,
|
||||
acct: 'boa',
|
||||
category: 1,
|
||||
};
|
||||
|
||||
const { sqlDependencies } = compile(`
|
||||
=from transactions
|
||||
where date >= 20170101 and date <= 20170131 and
|
||||
category = 1
|
||||
select { amount }
|
||||
`);
|
||||
const where = sqlDependencies[0].where;
|
||||
|
||||
expect(sqlinterp(where, transJan, 'transactions')).toBe(true);
|
||||
expect(sqlinterp(where, transFeb, 'transactions')).toBe(false);
|
||||
});
|
||||
@@ -12,28 +12,3 @@ export function unresolveName(name) {
|
||||
export function resolveName(sheet, name) {
|
||||
return sheet + '!' + name;
|
||||
}
|
||||
|
||||
export function resolveNamesAsObjects(sheets) {
|
||||
const cells = {};
|
||||
Object.keys(sheets).forEach(sheetName => {
|
||||
const sheet = sheets[sheetName];
|
||||
|
||||
Object.keys(sheet).forEach(name => {
|
||||
const expr = sheet[name];
|
||||
cells[resolveName(sheetName, name)] = expr;
|
||||
});
|
||||
});
|
||||
return cells;
|
||||
}
|
||||
|
||||
export function resolveNamesAsArrays(sheets) {
|
||||
const cells = [];
|
||||
Object.keys(sheets).forEach(sheetName => {
|
||||
const sheet = sheets[sheetName];
|
||||
|
||||
sheet.forEach(name => {
|
||||
cells.push(resolveName(sheetName, name));
|
||||
});
|
||||
});
|
||||
return cells;
|
||||
}
|
||||
|
||||
@@ -76,19 +76,6 @@ export function getTestKeyError({ reason }) {
|
||||
}
|
||||
}
|
||||
|
||||
export function getSubscribeError({ reason }) {
|
||||
switch (reason) {
|
||||
case 'network':
|
||||
return 'Unable to reach the server. Check your internet connection';
|
||||
case 'exists':
|
||||
return 'An account with that email already exists. Did you mean to login?';
|
||||
case 'invalid-email':
|
||||
return 'Invalid email';
|
||||
default:
|
||||
return 'An error occurred. Please try again later.';
|
||||
}
|
||||
}
|
||||
|
||||
export function getSyncError(error, id) {
|
||||
if (error === 'out-of-sync-migrations' || error === 'out-of-sync-data') {
|
||||
return 'This budget cannot be loaded with this version of the app.';
|
||||
|
||||
@@ -25,19 +25,6 @@ export const TYPE_INFO = {
|
||||
},
|
||||
};
|
||||
|
||||
export type FieldTypes = {
|
||||
imported_payee: string;
|
||||
payee: string;
|
||||
date: string;
|
||||
notes: string;
|
||||
amount: number;
|
||||
amountInflow: number;
|
||||
amountOutfow: number;
|
||||
category: string;
|
||||
account: string;
|
||||
cleared: boolean;
|
||||
};
|
||||
|
||||
export const FIELD_TYPES = new Map(
|
||||
Object.entries({
|
||||
imported_payee: 'string',
|
||||
|
||||
@@ -1,50 +1,7 @@
|
||||
export function cleanUUID(uuid) {
|
||||
return uuid.replace(/-/g, '');
|
||||
}
|
||||
|
||||
export function last(arr) {
|
||||
return arr[arr.length - 1];
|
||||
}
|
||||
|
||||
export function mergeObjects(objects) {
|
||||
return Object.assign.apply(null, [{}, ...objects]);
|
||||
}
|
||||
|
||||
export function composeCellChanges(objects) {
|
||||
const merged = {};
|
||||
Object.keys(objects).forEach(key => {
|
||||
if (merged[key]) {
|
||||
merged[key] = { ...merged[key], ...objects[key] };
|
||||
} else {
|
||||
merged[key] = objects[key];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function flattenArray(arrays) {
|
||||
return Array.prototype.concat.apply([], arrays);
|
||||
}
|
||||
|
||||
export function shallowEqual(a, b) {
|
||||
if (a === b) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let numKeysA = 0,
|
||||
numKeysB = 0,
|
||||
key;
|
||||
for (key in b) {
|
||||
numKeysB++;
|
||||
if (!a.hasOwnProperty(key) || a[key] !== b[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (key in a) {
|
||||
numKeysA++;
|
||||
}
|
||||
return numKeysA === numKeysB;
|
||||
}
|
||||
|
||||
export function getChangedValues(obj1, obj2) {
|
||||
// Keep the id field because this is mostly used to diff database
|
||||
// objects
|
||||
@@ -132,19 +89,6 @@ export function groupBy(data, field, mapper?: (v: unknown) => unknown) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function groupBySingle(data, field, mapper) {
|
||||
let res = new Map();
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
let item = data[i];
|
||||
let key = item[field];
|
||||
if (res.has(key)) {
|
||||
throw new Error('groupBySingle found conflicting key: ' + key);
|
||||
}
|
||||
res.set(key, mapper ? mapper(item) : data[i]);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
// This should replace the existing `groupById` function, since a
|
||||
// `Map` is better, but we can't swap it out because `Map` has a
|
||||
// different API and we need to go through and update everywhere that
|
||||
@@ -192,14 +136,6 @@ export function groupById(data) {
|
||||
return res;
|
||||
}
|
||||
|
||||
export function debugMemoFailure(prevProps, nextProps) {
|
||||
let changed = getChangedValues(prevProps, nextProps);
|
||||
if (changed !== null) {
|
||||
console.log(changed);
|
||||
}
|
||||
return changed === null;
|
||||
}
|
||||
|
||||
export function setIn(map, keys, item) {
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
let key = keys[i];
|
||||
@@ -228,11 +164,6 @@ export function getIn(map, keys) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// Useful for throwing exception from expressions
|
||||
export function throwError(err) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
export function fastSetMerge(set1, set2) {
|
||||
let finalSet = new Set(set1);
|
||||
let iter = set2.values();
|
||||
@@ -339,10 +270,6 @@ export function toRelaxedNumber(value) {
|
||||
return integerToAmount(currencyToInteger(value) || 0);
|
||||
}
|
||||
|
||||
export function toRelaxedInteger(value) {
|
||||
return stringToInteger(value) || 0;
|
||||
}
|
||||
|
||||
export function integerToCurrency(n) {
|
||||
return numberFormat.formatter.format(safeNumber(n) / 100);
|
||||
}
|
||||
@@ -406,15 +333,3 @@ export function looselyParseAmount(amount) {
|
||||
|
||||
return safeNumber(parseFloat(left + '.' + right));
|
||||
}
|
||||
|
||||
export function semverToNumber(str) {
|
||||
return parseInt(
|
||||
'1' +
|
||||
str
|
||||
.split('.')
|
||||
.map(x => {
|
||||
return ('000' + x.replace(/[^0-9]/g, '')).slice(-3);
|
||||
})
|
||||
.join(''),
|
||||
);
|
||||
}
|
||||
|
||||
6
upcoming-release-notes/1158.md
Normal file
6
upcoming-release-notes/1158.md
Normal file
@@ -0,0 +1,6 @@
|
||||
---
|
||||
category: Maintenance
|
||||
authors: [Shazib]
|
||||
---
|
||||
|
||||
Remove unused/legacy code from codebase
|
||||
Reference in New Issue
Block a user