Skip to content
Snippets Groups Projects
Unverified Commit 0c0f9e6c authored by Shazib Hussain's avatar Shazib Hussain Committed by GitHub
Browse files

Deleting all unused files, deleting unused functions from loot-core (#1158)

Last one before I add the actual linter rules!
parent d6ed860b
No related branches found
No related tags found
No related merge requests found
Showing
with 0 additions and 2564 deletions
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>
// );
// }
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;
export function getModalRoute(name: string): [string, string] {
let parts = name.split('/');
return [parts[0], parts.slice(1).join('/')];
}
import * as models from './models'; 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 = { export const accountModel = {
...models.accountModel, ...models.accountModel,
......
...@@ -1118,7 +1118,3 @@ export function generateSQLWithState( ...@@ -1118,7 +1118,3 @@ export function generateSQLWithState(
let { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig); let { sqlPieces, state } = compileQuery(queryState, schema, schemaConfig);
return { sql: defaultConstructQuery(queryState, state, sqlPieces), state }; return { sql: defaultConstructQuery(queryState, state, sqlPieces), state };
} }
export function generateSQL(queryState) {
return generateSQLWithState(queryState).sql;
}
...@@ -56,7 +56,3 @@ export function FileDownloadError(reason, meta?) { ...@@ -56,7 +56,3 @@ export function FileDownloadError(reason, meta?) {
export function FileUploadError(reason, meta?) { export function FileUploadError(reason, meta?) {
return { type: 'FileUploadError', reason, meta }; return { type: 'FileUploadError', reason, meta };
} }
export function isCodeError(err) {
return err instanceof ReferenceError || err instanceof SyntaxError;
}
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();
}
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) { export function number(v) {
if (typeof v === 'number') { if (typeof v === 'number') {
return v; return v;
...@@ -20,11 +11,3 @@ export function number(v) { ...@@ -20,11 +11,3 @@ export function number(v) {
return 0; return 0;
} }
export function min(x, y) {
return Math.min(x, y);
}
export function max(x, y) {
return Math.max(x, y);
}
* 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)
// 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",
},
],
]
`;
// 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": "
",
},
]
`;
// 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,
}
`;
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();
});
});
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);
}
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;
}
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();
});
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);
}
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';
}
}
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;
},
};
}
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]);
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment