diff --git a/package.json b/package.json index 9320747286..2f128a0e49 100644 --- a/package.json +++ b/package.json @@ -20,12 +20,16 @@ }, "husky": { "hooks": { - "pre-commit": "yarn format && CI=true yarn test", - "pre-push": "yarn format && CI=true yarn test" + "pre-commit": "yarn format:ci && CI=true yarn test", + "pre-push": "yarn format:ci && CI=true yarn test" } }, "dependencies": { "@blueprintjs/core": "^2.1.1", + "@types/acorn": "^4.0.3", + "astring": "^1.3.0", + "common-tags": "^1.7.2", + "flexboxgrid": "^6.3.1", "normalize.css": "^8.0.0", "react": "^16.3.1", "react-ace": "^6.1.1", @@ -37,13 +41,17 @@ "react-transition-group": "^2.3.1", "redux": "^3.7.2", "redux-mock-store": "^1.5.1", + "redux-saga": "^0.15.6", "typesafe-actions": "^1.1.2", "utility-types": "^2.0.0" }, "devDependencies": { "@types/classnames": "^2.2.3", + "@types/common-tags": "^1.4.0", "@types/enzyme": "^3.1.9", "@types/enzyme-adapter-react-16": "^1.0.2", + "@types/estree": "^0.0.39", + "@types/invariant": "^2.2.29", "@types/jest": "^22.2.3", "@types/node": "^9.6.5", "@types/react": "^16.3.10", diff --git a/src/actions/actionTypes.ts b/src/actions/actionTypes.ts new file mode 100644 index 0000000000..536d1fdee9 --- /dev/null +++ b/src/actions/actionTypes.ts @@ -0,0 +1,22 @@ +import { Action as ReduxAction } from 'redux' + +export interface IAction extends ReduxAction { + payload: any +} + +/** Playground */ +export const UPDATE_EDITOR_VALUE = 'UPDATE_EDITOR_VALUE' +export const UPDATE_REPL_VALUE = 'UPDATE_REPL_VALUE' +export const EVAL_EDITOR = 'EVAL_EDITOR' +export const EVAL_REPL = 'EVAL_REPL' +export const CLEAR_REPL_INPUT = 'CLEAR_REPL_INPUT' +export const CLEAR_REPL_OUTPUT = 'CLEAR_REPL_OUTPUT' +export const CLEAR_CONTEXT = 'CLEAR_CONTEXT' +export const SEND_REPL_INPUT_TO_OUTPUT = 'SEND_REPL_INPUT_TO_OUTPUT' + +/** Interpreter */ +export const HANDLE_CONSOLE_LOG = 'HANDLE_CONSOLE_LOG' +export const EVAL_INTERPRETER = 'EVAL_INTERPRETER' +export const EVAL_INTERPRETER_SUCCESS = 'EVAL_INTERPRETER_SUCCESS' +export const EVAL_INTERPRETER_ERROR = 'EVAL_INTERPRETER_ERROR' +export const INTERRUPT_EXECUTION = 'INTERRUPT_EXECUTION' diff --git a/src/actions/index.ts b/src/actions/index.ts new file mode 100644 index 0000000000..70c274749c --- /dev/null +++ b/src/actions/index.ts @@ -0,0 +1,2 @@ +export * from './interpreter' +export * from './playground' diff --git a/src/actions/interpreter.ts b/src/actions/interpreter.ts new file mode 100644 index 0000000000..9daa06d4d7 --- /dev/null +++ b/src/actions/interpreter.ts @@ -0,0 +1,26 @@ +import { SourceError, Value } from '../slang/types' +import * as actionTypes from './actionTypes' + +export const handleConsoleLog = (log: string) => ({ + type: actionTypes.HANDLE_CONSOLE_LOG, + payload: log +}) + +export const evalInterpreter = (code: string) => ({ + type: actionTypes.EVAL_INTERPRETER, + payload: code +}) + +export const evalInterpreterSuccess = (value: Value) => ({ + type: actionTypes.EVAL_INTERPRETER_SUCCESS, + payload: { type: 'result', value } +}) + +export const evalInterpreterError = (errors: SourceError[]) => ({ + type: actionTypes.EVAL_INTERPRETER_ERROR, + payload: { type: 'errors', errors } +}) + +export const handleInterruptExecution = () => ({ + type: actionTypes.INTERRUPT_EXECUTION +}) diff --git a/src/actions/playground.ts b/src/actions/playground.ts index 4f3153f54c..c900fe6437 100644 --- a/src/actions/playground.ts +++ b/src/actions/playground.ts @@ -1,26 +1,40 @@ -import { Action, ActionCreator } from 'redux' - -/** - * The `type` attribute for an `Action` which updates the `IPlaygroundState` - * `editorValue` - */ -export const UPDATE_EDITOR_VALUE = 'UPDATE_EDITOR_VALUE' - -/** - * Represents an `Action` which updates the `editorValue` of a - * `IPlaygroundState` - * @property type - Unique string identifier for this `Action` - * @property newEditorValue - The new string value for `editorValue` - */ -export interface IUpdateEditorValue extends Action { - payload: string -} - -/** - * An `ActionCreator` returning an `IUpdateEditorValue` `Action` - * @param newEditorValue - The new string value for `editorValue` - */ -export const updateEditorValue: ActionCreator = (newEditorValue: string) => ({ - type: UPDATE_EDITOR_VALUE, +import { ActionCreator } from 'redux' +import * as actionTypes from './actionTypes' + +export const updateEditorValue: ActionCreator = (newEditorValue: string) => ({ + type: actionTypes.UPDATE_EDITOR_VALUE, payload: newEditorValue }) + +export const updateReplValue: ActionCreator = (newReplValue: string) => ({ + type: actionTypes.UPDATE_REPL_VALUE, + payload: newReplValue +}) + +export const sendReplInputToOutput: ActionCreator = (newOutput: string) => ({ + type: actionTypes.SEND_REPL_INPUT_TO_OUTPUT, + payload: { + type: 'code', + value: newOutput + } +}) + +export const evalEditor = () => ({ + type: actionTypes.EVAL_EDITOR +}) + +export const evalRepl = () => ({ + type: actionTypes.EVAL_REPL +}) + +export const clearReplInput = () => ({ + type: actionTypes.CLEAR_REPL_INPUT +}) + +export const clearReplOutput = () => ({ + type: actionTypes.CLEAR_REPL_OUTPUT +}) + +export const clearContext = () => ({ + type: actionTypes.CLEAR_CONTEXT +}) diff --git a/src/components/IDE/Control.tsx b/src/components/IDE/Control.tsx new file mode 100644 index 0000000000..39523903ca --- /dev/null +++ b/src/components/IDE/Control.tsx @@ -0,0 +1,50 @@ +import * as React from 'react' + +import { Button, IconName, Intent } from '@blueprintjs/core' + +/** + * @property handleEvalEditor - A callback function for evaluation + * of the editor's content, using `slang` + */ +export interface IControlProps { + handleEvalEditor: () => void + handleEvalRepl: () => void + handleClearReplOutput: () => void +} + +class Control extends React.Component { + public render() { + const genericButton = ( + label: string, + icon: IconName, + handleClick = () => {}, + intent = Intent.NONE, + notMinimal = false + ) => ( + + ) + const runButton = genericButton('Run', 'play', this.props.handleEvalEditor) + const evalButton = genericButton('Eval', 'play', this.props.handleEvalRepl) + const clearButton = genericButton('Clear', 'remove', this.props.handleClearReplOutput) + return ( +
+
{runButton}
+
+
+
{evalButton}
+
{clearButton}
+
+
+
+ ) + } +} + +export default Control diff --git a/src/components/IDE/Editor.tsx b/src/components/IDE/Editor.tsx new file mode 100644 index 0000000000..05232ac4a2 --- /dev/null +++ b/src/components/IDE/Editor.tsx @@ -0,0 +1,33 @@ +import * as React from 'react' + +import AceEditor from 'react-ace' + +import 'brace/mode/javascript' +import 'brace/theme/terminal' + +/** + * @property editorValue - The string content of the react-ace editor + * @property handleEditorChange - A callback function + * for the react-ace editor's `onChange` + */ +export interface IEditorProps { + editorValue: string + handleEditorChange: (newCode: string) => void +} + +class Editor extends React.Component { + public render() { + return ( + + ) + } +} + +export default Editor diff --git a/src/components/IDE/Repl.tsx b/src/components/IDE/Repl.tsx new file mode 100644 index 0000000000..5b1014b5b2 --- /dev/null +++ b/src/components/IDE/Repl.tsx @@ -0,0 +1,59 @@ +import * as React from 'react' + +import { Card } from '@blueprintjs/core' +import ReplInputContainer from '../../containers/IDE/ReplInputContainer' +import { InterpreterOutput } from '../../reducers/states' +import { parseError, toString } from '../../slang' + +export interface IReplProps { + output: InterpreterOutput[] +} + +export interface IOutputProps { + output: InterpreterOutput +} + +const Repl: React.SFC = props => { + const cards = props.output.map((slice, index) => ) + return ( +
+ {cards} +
+ +
+
+ ) +} + +export const Output: React.SFC = props => { + switch (props.output.type) { + case 'code': + return {props.output.value} + case 'running': + return {props.output.consoleLogs.join('\n')} + case 'result': + if (props.output.consoleLogs.length === 0) { + return {toString(props.output.value)} + } else { + return ( + + {[props.output.consoleLogs.join('\n'), toString(props.output.value)].join('\n')} + + ) + } + case 'errors': + if (props.output.consoleLogs.length === 0) { + return {parseError(props.output.errors)} + } else { + return ( + + {[props.output.consoleLogs.join('\n'), parseError(props.output.errors)].join('\n')} + + ) + } + default: + return '' + } +} + +export default Repl diff --git a/src/components/IDE/ReplInput.tsx b/src/components/IDE/ReplInput.tsx new file mode 100644 index 0000000000..482a692f7b --- /dev/null +++ b/src/components/IDE/ReplInput.tsx @@ -0,0 +1,41 @@ +import * as React from 'react' +import AceEditor from 'react-ace' + +import 'brace/mode/javascript' +import 'brace/theme/terminal' + +export interface IReplInputProps { + replValue: string + handleReplChange: (newCode: string) => void + handleReplEval: () => void +} + +class ReplInput extends React.Component { + public render() { + return ( + + ) + } +} + +export default ReplInput diff --git a/src/components/IDE/__tests__/Control.tsx b/src/components/IDE/__tests__/Control.tsx new file mode 100644 index 0000000000..2a2c8646b3 --- /dev/null +++ b/src/components/IDE/__tests__/Control.tsx @@ -0,0 +1,16 @@ +import * as React from 'react' + +import { shallow } from 'enzyme' + +import Control, { IControlProps } from '../Control' + +test('Control renders correctly', () => { + const props: IControlProps = { + handleEvalEditor: () => {}, + handleEvalRepl: () => {}, + handleClearReplOutput: () => {} + } + const app = + const tree = shallow(app) + expect(tree.debug()).toMatchSnapshot() +}) diff --git a/src/components/IDE/__tests__/Editor.tsx b/src/components/IDE/__tests__/Editor.tsx new file mode 100644 index 0000000000..b6e7841122 --- /dev/null +++ b/src/components/IDE/__tests__/Editor.tsx @@ -0,0 +1,15 @@ +import * as React from 'react' + +import { shallow } from 'enzyme' + +import Editor, { IEditorProps } from '../Editor' + +test('Editor renders correctly', () => { + const props: IEditorProps = { + editorValue: '', + handleEditorChange: newCode => {} + } + const app = + const tree = shallow(app) + expect(tree.debug()).toMatchSnapshot() +}) diff --git a/src/components/IDE/__tests__/Repl.tsx b/src/components/IDE/__tests__/Repl.tsx new file mode 100644 index 0000000000..f7687d6c09 --- /dev/null +++ b/src/components/IDE/__tests__/Repl.tsx @@ -0,0 +1,21 @@ +import { shallow } from 'enzyme' +import * as React from 'react' + +import { ResultOutput } from '../../../reducers/states' +import Repl, { Output } from '../Repl' + +test('Repl renders correctly', () => { + const props = { + output: [{ type: 'result', value: 'abc', consoleLogs: [] } as ResultOutput] + } + const app = + const tree = shallow(app) + expect(tree.debug()).toMatchSnapshot() +}) + +test("Output renders correctly for InterpreterOutput.type === 'result'", () => { + const props: ResultOutput = { type: 'result', value: 'def', consoleLogs: [] } + const app = + const tree = shallow(app) + expect(tree.debug()).toMatchSnapshot() +}) diff --git a/src/components/IDE/__tests__/__snapshots__/Control.tsx.snap b/src/components/IDE/__tests__/__snapshots__/Control.tsx.snap new file mode 100644 index 0000000000..162ba7418a --- /dev/null +++ b/src/components/IDE/__tests__/__snapshots__/Control.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Control renders correctly 1`] = ` +"
+
+ + Run + +
+
+
+
+ + Eval + +
+
+ + Clear + +
+
+
+
" +`; diff --git a/src/components/IDE/__tests__/__snapshots__/Editor.tsx.snap b/src/components/IDE/__tests__/__snapshots__/Editor.tsx.snap new file mode 100644 index 0000000000..b26810c699 --- /dev/null +++ b/src/components/IDE/__tests__/__snapshots__/Editor.tsx.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Editor renders correctly 1`] = `""`; diff --git a/src/components/IDE/__tests__/__snapshots__/Repl.tsx.snap b/src/components/IDE/__tests__/__snapshots__/Repl.tsx.snap new file mode 100644 index 0000000000..52c9cb8358 --- /dev/null +++ b/src/components/IDE/__tests__/__snapshots__/Repl.tsx.snap @@ -0,0 +1,16 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Output renders correctly for InterpreterOutput.type === 'result' 1`] = ` +" + "def" +" +`; + +exports[`Repl renders correctly 1`] = ` +"
+ +
+ +
+
" +`; diff --git a/src/components/IDE/index.tsx b/src/components/IDE/index.tsx new file mode 100644 index 0000000000..9b09d8cf62 --- /dev/null +++ b/src/components/IDE/index.tsx @@ -0,0 +1,21 @@ +import * as React from 'react' + +import ControlContainer from '../../containers/IDE/ControlContainer' +import EditorContainer from '../../containers/IDE/EditorContainer' +import ReplContainer from '../../containers/IDE/ReplContainer' + +const IDE: React.SFC<{}> = () => ( +
+
+
+ +
+
+
+ + +
+
+) + +export default IDE diff --git a/src/components/Playground.tsx b/src/components/Playground.tsx index 834b0eed58..3ab5577480 100644 --- a/src/components/Playground.tsx +++ b/src/components/Playground.tsx @@ -1,11 +1,12 @@ import * as React from 'react' -import EditorContainer from '../containers/EditorContainer' -const Playground: React.SFC<{}> = ()=> { +import IDE from './IDE' + +const Playground: React.SFC<{}> = () => { return (

Playground

- +
) } diff --git a/src/components/__tests__/Editor.tsx b/src/components/__tests__/Editor.tsx index ab5f918122..b6e7841122 100644 --- a/src/components/__tests__/Editor.tsx +++ b/src/components/__tests__/Editor.tsx @@ -6,10 +6,10 @@ import Editor, { IEditorProps } from '../Editor' test('Editor renders correctly', () => { const props: IEditorProps = { - editorValue: '' - handleEditorChange: (newCode) => + editorValue: '', + handleEditorChange: newCode => {} } - const app = + const app = const tree = shallow(app) expect(tree.debug()).toMatchSnapshot() }) diff --git a/src/components/__tests__/__snapshots__/Playground.tsx.snap b/src/components/__tests__/__snapshots__/Playground.tsx.snap index feff1937b8..2145abc222 100644 --- a/src/components/__tests__/__snapshots__/Playground.tsx.snap +++ b/src/components/__tests__/__snapshots__/Playground.tsx.snap @@ -5,6 +5,6 @@ exports[`Playground renders correctly 1`] = `

Playground

- + " `; diff --git a/src/containers/ApplicationContainer.ts b/src/containers/ApplicationContainer.ts index 357b369d3f..60d474c7cd 100644 --- a/src/containers/ApplicationContainer.ts +++ b/src/containers/ApplicationContainer.ts @@ -1,7 +1,7 @@ import { connect, MapStateToProps } from 'react-redux' import { withRouter } from 'react-router' import Application from '../components/Application' -import { IState } from '../reducers' +import { IState } from '../reducers/states' /** * Provides the title of the application for display. diff --git a/src/containers/IDE/ControlContainer.ts b/src/containers/IDE/ControlContainer.ts new file mode 100644 index 0000000000..bf0102f302 --- /dev/null +++ b/src/containers/IDE/ControlContainer.ts @@ -0,0 +1,28 @@ +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux' +import { bindActionCreators, Dispatch } from 'redux' + +import { clearReplOutput, evalEditor, evalRepl } from '../../actions/playground' +import Control, { IControlProps } from '../../components/IDE/Control' +import { IState } from '../../reducers/states' + +type DispatchProps = Pick & + Pick & + Pick + +/** No-op mapStateToProps */ +const mapStateToProps: MapStateToProps<{}, {}, IState> = state => ({}) + +/** Provides a callback function `handleEvalEditor` + * to evaluate code in the Editor. + */ +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( + { + handleEvalEditor: evalEditor, + handleEvalRepl: evalRepl, + handleClearReplOutput: clearReplOutput + }, + dispatch + ) + +export default connect(mapStateToProps, mapDispatchToProps)(Control) diff --git a/src/containers/EditorContainer.ts b/src/containers/IDE/EditorContainer.ts similarity index 68% rename from src/containers/EditorContainer.ts rename to src/containers/IDE/EditorContainer.ts index 9fd9c75d4b..2869ecb231 100644 --- a/src/containers/EditorContainer.ts +++ b/src/containers/IDE/EditorContainer.ts @@ -1,9 +1,9 @@ import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux' -import { Dispatch } from 'redux' +import { bindActionCreators, Dispatch } from 'redux' -import { updateEditorValue } from '../actions/playground' -import Editor, { IEditorProps } from '../components/Editor' -import { IState } from '../reducers' +import { updateEditorValue } from '../../actions/playground' +import Editor, { IEditorProps } from '../../components/IDE/Editor' +import { IState } from '../../reducers/states' type StateProps = Pick type DispatchProps = Pick @@ -21,12 +21,12 @@ const mapStateToProps: MapStateToProps = state => { * `updateEditorValue` with `newCode`, the updated contents of the react-ace * editor. */ -const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => { - return { - handleEditorChange: (newCode: string) => { - dispatch(updateEditorValue(newCode)) - } - } -} +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( + { + handleEditorChange: updateEditorValue + }, + dispatch + ) export default connect(mapStateToProps, mapDispatchToProps)(Editor) diff --git a/src/containers/IDE/ReplContainer.ts b/src/containers/IDE/ReplContainer.ts new file mode 100644 index 0000000000..6cc254eadc --- /dev/null +++ b/src/containers/IDE/ReplContainer.ts @@ -0,0 +1,12 @@ +import { connect, MapStateToProps } from 'react-redux' + +import Repl, { IReplProps } from '../../components/IDE/Repl' +import { IState } from '../../reducers/states' + +const mapStateToProps: MapStateToProps = state => { + return { + output: state.playground.output + } +} + +export default connect(mapStateToProps)(Repl) diff --git a/src/containers/IDE/ReplInputContainer.ts b/src/containers/IDE/ReplInputContainer.ts new file mode 100644 index 0000000000..e78162e1fa --- /dev/null +++ b/src/containers/IDE/ReplInputContainer.ts @@ -0,0 +1,27 @@ +import { connect, MapDispatchToProps, MapStateToProps } from 'react-redux' +import { bindActionCreators, Dispatch } from 'redux' + +import { evalRepl, updateReplValue } from '../../actions/playground' +import ReplInput, { IReplInputProps } from '../../components/IDE/ReplInput' +import { IState } from '../../reducers/states' + +type StateProps = Pick +type DispatchProps = Pick & + Pick + +const mapStateToProps: MapStateToProps = state => { + return { + replValue: state.playground.replValue + } +} + +const mapDispatchToProps: MapDispatchToProps = (dispatch: Dispatch) => + bindActionCreators( + { + handleReplChange: updateReplValue, + handleReplEval: evalRepl + }, + dispatch + ) + +export default connect(mapStateToProps, mapDispatchToProps)(ReplInput) diff --git a/src/createStore.ts b/src/createStore.ts index 0a89532061..4e311d1695 100644 --- a/src/createStore.ts +++ b/src/createStore.ts @@ -8,25 +8,32 @@ import { Store, StoreEnhancer } from 'redux' -import reducers, { IState } from './reducers' +import reducers from './reducers' +import { IState } from './reducers/states' -declare var __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: () => StoreEnhancer +import createSagaMiddleware from 'redux-saga' +import mainSaga from './sagas' -export default function createStore(history: History): Store { - const middleware = [routerMiddleware(history)] +declare var __REDUX_DEVTOOLS_EXTENSION_COMPOSE__: () => StoreEnhancer +function createStore(history: History): Store { let composeEnhancers: any = compose + const sagaMiddleware = createSagaMiddleware() + const middleware = [sagaMiddleware, routerMiddleware(history)] + if (typeof __REDUX_DEVTOOLS_EXTENSION_COMPOSE__ === 'function') { composeEnhancers = __REDUX_DEVTOOLS_EXTENSION_COMPOSE__ } + const rootReducer = combineReducers({ + ...reducers, + router: routerReducer + }) const enchancers = composeEnhancers(applyMiddleware(...middleware)) + const createdStore = _createStore(rootReducer, enchancers) - return _createStore( - combineReducers({ - ...reducers, - router: routerReducer - }), - enchancers - ) + sagaMiddleware.run(mainSaga) + return createdStore } + +export default createStore diff --git a/src/index.tsx b/src/index.tsx index 66cbe98ef0..b9ef940dca 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -4,16 +4,19 @@ import { createBrowserHistory } from 'history' import { render } from 'react-dom' import { Provider } from 'react-redux' import { ConnectedRouter } from 'react-router-redux' +import { Store } from 'redux' import ApplicationContainer from './containers/ApplicationContainer' import createStore from './createStore' +import { IState } from './reducers/states' import registerServiceWorker from './registerServiceWorker' import './styles/index.css' const rootContainer = document.getElementById('root') as HTMLElement const history = createBrowserHistory() -const store = createStore(history) +const store = createStore(history) as Store +;(window as any).__REDUX_STORE__ = store // need this for slang's display render( diff --git a/src/mocks/context.ts b/src/mocks/context.ts new file mode 100644 index 0000000000..12ecd4c6f7 --- /dev/null +++ b/src/mocks/context.ts @@ -0,0 +1,6 @@ +import { createContext } from '../slang' +import { Context } from '../slang/types' + +export function mockContext(): Context { + return createContext() +} diff --git a/src/mocks/store.ts b/src/mocks/store.ts index d2656b1a7a..b5618a61eb 100644 --- a/src/mocks/store.ts +++ b/src/mocks/store.ts @@ -1,19 +1,13 @@ import { Store } from 'redux' import * as mockStore from 'redux-mock-store' -import { IState } from '../reducers' -import { ApplicationEnvironment } from '../reducers/application' +import { defaultApplication, defaultPlayground, IState } from '../reducers/states' export function mockInitialStore

(): Store { const createStore = (mockStore as any)() const state: IState = { - application: { - title: 'Cadet', - environment: ApplicationEnvironment.Development - }, - playground: { - editorValue: '' - } + application: defaultApplication, + playground: defaultPlayground } return createStore(state) } diff --git a/src/notification.ts b/src/notification.ts new file mode 100644 index 0000000000..74fa1170fe --- /dev/null +++ b/src/notification.ts @@ -0,0 +1,21 @@ +import { Intent, Position, Toaster } from '@blueprintjs/core' + +const notification = Toaster.create({ + position: Position.TOP +}) + +export const showSuccessMessage = (message: string, timeout = 500) => { + notification.show({ + intent: Intent.SUCCESS, + message, + timeout + }) +} + +export const showWarningMessage = (message: string, timeout = 500) => { + notification.show({ + intent: Intent.WARNING, + message, + timeout + }) +} diff --git a/src/reducers/application.ts b/src/reducers/application.ts index 6885552c2a..01839a5ebe 100644 --- a/src/reducers/application.ts +++ b/src/reducers/application.ts @@ -1,32 +1,6 @@ import { Action, Reducer } from 'redux' +import { defaultApplication, IApplicationState } from './states' -export enum ApplicationEnvironment { - Development = 'development', - Production = 'production', - Test = 'test' -} - -export interface IApplicationState { - title: string - environment: ApplicationEnvironment -} - -const currentEnvironment = (): ApplicationEnvironment => { - switch (process.env.NODE_ENV) { - case 'development': - return ApplicationEnvironment.Development - case 'production': - return ApplicationEnvironment.Production - default: - return ApplicationEnvironment.Test - } -} - -const defaultState: IApplicationState = { - title: 'Cadet', - environment: currentEnvironment() -} - -export const reducer: Reducer = (state = defaultState, action: Action) => { +export const reducer: Reducer = (state = defaultApplication, action: Action) => { return state } diff --git a/src/reducers/index.ts b/src/reducers/index.ts index 7b3e015371..40d00ed68b 100644 --- a/src/reducers/index.ts +++ b/src/reducers/index.ts @@ -1,10 +1,5 @@ -import { IApplicationState, reducer as application } from './application' -import { IPlaygroundState, reducer as playground } from './playground' - -export interface IState { - readonly application: IApplicationState - readonly playground: IPlaygroundState -} +import { reducer as application } from './application' +import { reducer as playground } from './playground' export default { application, diff --git a/src/reducers/playground.ts b/src/reducers/playground.ts index 4fe3f957c4..c42e4a45ab 100644 --- a/src/reducers/playground.ts +++ b/src/reducers/playground.ts @@ -1,28 +1,106 @@ -import { Action, Reducer } from 'redux' -import { IUpdateEditorValue, UPDATE_EDITOR_VALUE } from '../actions/playground' +import { Reducer } from 'redux' +import { + CLEAR_CONTEXT, + CLEAR_REPL_INPUT, + CLEAR_REPL_OUTPUT, + EVAL_INTERPRETER_ERROR, + EVAL_INTERPRETER_SUCCESS, + HANDLE_CONSOLE_LOG, + IAction, + SEND_REPL_INPUT_TO_OUTPUT, + UPDATE_EDITOR_VALUE, + UPDATE_REPL_VALUE +} from '../actions/actionTypes' +import { createContext } from '../slang' +import { CodeOutput, defaultPlayground, InterpreterOutput, IPlaygroundState } from './states' -export interface IPlaygroundState { - editorValue: string -} - -/** - * The default (initial) state of the `IPlaygroundState` - */ -const defaultState: IPlaygroundState = { - editorValue: '' -} - -/** - * The reducer for `IPlaygroundState` - * - * UPDATE_EDITOR_VALUE: Update the `editorValue` property - */ -export const reducer: Reducer = (state = defaultState, action: Action) => { +export const reducer: Reducer = (state = defaultPlayground, action: IAction) => { + let newOutput: InterpreterOutput[] + let lastOutput: InterpreterOutput switch (action.type) { case UPDATE_EDITOR_VALUE: return { ...state, - editorValue: (action as IUpdateEditorValue).payload + editorValue: action.payload + } + case UPDATE_REPL_VALUE: + return { + ...state, + replValue: action.payload + } + case CLEAR_REPL_INPUT: + return { + ...state, + replValue: '' + } + case CLEAR_REPL_OUTPUT: + return { + ...state, + output: [] + } + case CLEAR_CONTEXT: + return { + ...state, + context: createContext() + } + case HANDLE_CONSOLE_LOG: + /* Possible cases: + * (1) state.output === [], i.e. state.output[-1] === undefined + * (2) state.output[-1] is not RunningOutput + * (3) state.output[-1] is RunningOutput */ + lastOutput = state.output.slice(-1)[0] + if (lastOutput === undefined || lastOutput.type !== 'running') { + newOutput = state.output.concat({ + type: 'running', + consoleLogs: [action.payload] + }) + } else { + lastOutput.consoleLogs = lastOutput.consoleLogs.concat(action.payload) + newOutput = state.output.slice(0, -1).concat(lastOutput) + } + return { + ...state, + output: newOutput + } + case SEND_REPL_INPUT_TO_OUTPUT: + newOutput = state.output.concat(action.payload as CodeOutput) + return { + ...state, + output: newOutput + } + case EVAL_INTERPRETER_SUCCESS: + lastOutput = state.output.slice(-1)[0] + if (lastOutput !== undefined && lastOutput.type === 'running') { + newOutput = state.output.slice(0, -1).concat({ + ...action.payload, + consoleLogs: lastOutput.consoleLogs + }) + } else { + newOutput = state.output.concat({ + ...action.payload, + consoleLogs: [] + }) + } + return { + ...state, + output: newOutput + } + case EVAL_INTERPRETER_ERROR: + lastOutput = state.output.slice(-1)[0] + if (lastOutput !== undefined && lastOutput.type === 'running') { + newOutput = state.output.slice(0, -1).concat({ + ...action.payload, + consoleLogs: lastOutput.consoleLogs + }) + } else { + newOutput = state.output.concat({ + ...action.payload, + consoleLogs: [] + }) + } + return { + ...state, + output: newOutput } default: return state diff --git a/src/reducers/states.ts b/src/reducers/states.ts new file mode 100644 index 0000000000..435cf2f790 --- /dev/null +++ b/src/reducers/states.ts @@ -0,0 +1,94 @@ +import { Context, createContext } from '../slang' +import { SourceError } from '../slang/types' + +export interface IState { + readonly application: IApplicationState + readonly playground: IPlaygroundState +} + +export interface IApplicationState { + readonly title: string + readonly environment: ApplicationEnvironment +} + +export interface IPlaygroundState { + readonly editorValue: string + readonly replValue: string + readonly context: Context + readonly output: InterpreterOutput[] +} + +/** + * An output while the program is still being run in the interpreter. As a + * result, there are no return values or SourceErrors yet. However, there could + * have been calls to display (console.log) that need to be printed out. + */ +export type RunningOutput = { + type: 'running' + consoleLogs: string[] +} + +/** + * An output which reflects the program which the user had entered. Not a true + * Output from the interpreter, but simply there to let he user know what had + * been entered. + */ +export type CodeOutput = { + type: 'code' + value: string +} + +/** + * An output which represents a program being run successfully, i.e. with a + * return value at the end. A program can have either a return value, or errors, + * but not both. + */ +export type ResultOutput = { + type: 'result' + value: any + consoleLogs: string[] + runtime?: number + isProgram?: boolean +} + +/** + * An output which represents a program being run unsuccessfully, i.e. with + * errors at the end. A program can have either a return value, or errors, but + * not both. + */ +export type ErrorOutput = { + type: 'errors' + errors: SourceError[] + consoleLogs: string[] +} + +export type InterpreterOutput = RunningOutput | CodeOutput | ResultOutput | ErrorOutput + +export enum ApplicationEnvironment { + Development = 'development', + Production = 'production', + Test = 'test' +} + +const currentEnvironment = (): ApplicationEnvironment => { + switch (process.env.NODE_ENV) { + case 'development': + return ApplicationEnvironment.Development + case 'production': + return ApplicationEnvironment.Production + default: + return ApplicationEnvironment.Test + } +} + +export const defaultApplication: IApplicationState = { + title: 'Cadet', + environment: currentEnvironment() +} + +export const defaultPlayground: IPlaygroundState = { + editorValue: '', + replValue: '', + context: createContext(), + output: [] +} diff --git a/src/sagas/index.ts b/src/sagas/index.ts new file mode 100644 index 0000000000..de6a26988b --- /dev/null +++ b/src/sagas/index.ts @@ -0,0 +1,53 @@ +import { SagaIterator } from 'redux-saga' +import { call, put, race, select, take, takeEvery } from 'redux-saga/effects' +import { IState } from '../reducers/states' + +import { Context, interrupt, runInContext } from '../slang' + +import * as actions from '../actions' +import * as actionTypes from '../actions/actionTypes' + +function* evalCode(code: string, context: Context) { + const { result, interrupted } = yield race({ + result: call(runInContext, code, context), + interrupted: take(actionTypes.INTERRUPT_EXECUTION) + }) + if (result) { + if (result.status === 'finished') { + yield put(actions.evalInterpreterSuccess(result.value)) + } else { + yield put(actions.evalInterpreterError(context.errors)) + } + } else if (interrupted) { + interrupt(context) + } +} + +function* interpreterSaga(): SagaIterator { + // let library = yield select((state: Shape) => state.config.library) + let context: Context + + yield takeEvery(actionTypes.EVAL_EDITOR, function*() { + const code: string = yield select((state: IState) => state.playground.editorValue) + yield put(actions.handleInterruptExecution()) + yield put(actions.clearContext()) + yield put(actions.clearReplOutput()) + context = yield select((state: IState) => state.playground.context) + yield* evalCode(code, context) + }) + + yield takeEvery(actionTypes.EVAL_REPL, function*() { + const code: string = yield select((state: IState) => state.playground.replValue) + context = yield select((state: IState) => state.playground.context) + yield put(actions.handleInterruptExecution()) + yield put(actions.clearReplInput()) + yield put(actions.sendReplInputToOutput(code)) + yield* evalCode(code, context) + }) +} + +function* mainSaga() { + yield* interpreterSaga() +} + +export default mainSaga diff --git a/src/slang/__tests__/__snapshots__/index.ts.snap b/src/slang/__tests__/__snapshots__/index.ts.snap new file mode 100644 index 0000000000..c18adc1898 --- /dev/null +++ b/src/slang/__tests__/__snapshots__/index.ts.snap @@ -0,0 +1,159 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Arrow function definition returns itself 1`] = ` +Object { + "status": "finished", + "value": ArrowClosure { + "frame": Object { + "environment": Object { + "display": [Function], + "error": [Function], + "math_PI": 3.141592653589793, + "math_sqrt": [Function], + "parse_int": [Function], + "prompt": [Function], + "runtime": [Function], + "undefined": undefined, + }, + "name": "global", + "parent": null, + }, + "fun": [Function], + "name": "Anonymous1", + "node": Node { + "__id": "node_12", + "body": Node { + "__id": "node_11", + "end": 8, + "loc": SourceLocation { + "end": Position { + "column": 8, + "line": 1, + }, + "start": Position { + "column": 6, + "line": 1, + }, + }, + "raw": "42", + "start": 6, + "type": "Literal", + "value": 42, + }, + "end": 8, + "expression": true, + "generator": false, + "id": null, + "loc": SourceLocation { + "end": Position { + "column": 8, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "params": Array [], + "start": 0, + "type": "ArrowFunctionExpression", + }, + }, +} +`; + +exports[`Arrow function definition returns itself 2`] = ` +ArrowClosure { + "frame": Object { + "environment": Object { + "display": [Function], + "error": [Function], + "math_PI": 3.141592653589793, + "math_sqrt": [Function], + "parse_int": [Function], + "prompt": [Function], + "runtime": [Function], + "undefined": undefined, + }, + "name": "global", + "parent": null, + }, + "fun": [Function], + "name": "Anonymous1", + "node": Node { + "__id": "node_12", + "body": Node { + "__id": "node_11", + "end": 8, + "loc": SourceLocation { + "end": Position { + "column": 8, + "line": 1, + }, + "start": Position { + "column": 6, + "line": 1, + }, + }, + "raw": "42", + "start": 6, + "type": "Literal", + "value": 42, + }, + "end": 8, + "expression": true, + "generator": false, + "id": null, + "loc": SourceLocation { + "end": Position { + "column": 8, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "params": Array [], + "start": 0, + "type": "ArrowFunctionExpression", + }, +} +`; + +exports[`Empty code returns undefined 1`] = ` +Object { + "status": "finished", + "value": undefined, +} +`; + +exports[`Factorial arrow function 1`] = ` +Object { + "status": "finished", + "value": 120, +} +`; + +exports[`Single boolean self-evaluates to itself 1`] = ` +Object { + "status": "finished", + "value": true, +} +`; + +exports[`Single number self-evaluates to itself 1`] = ` +Object { + "status": "finished", + "value": 42, +} +`; + +exports[`Single string self-evaluates to itself 1`] = ` +Object { + "status": "finished", + "value": "42", +} +`; + +exports[`parseError for missing semicolon 1`] = `"Line 1: Missing semicolon at the end of statement"`; diff --git a/src/slang/__tests__/__snapshots__/parser.ts.snap b/src/slang/__tests__/__snapshots__/parser.ts.snap new file mode 100644 index 0000000000..2cd793aa2a --- /dev/null +++ b/src/slang/__tests__/__snapshots__/parser.ts.snap @@ -0,0 +1,135 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Empty parse returns empty Program Node 1`] = ` +Node { + "__id": "node_1", + "body": Array [], + "end": 0, + "loc": SourceLocation { + "end": Position { + "column": 0, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "sourceType": "script", + "start": 0, + "type": "Program", +} +`; + +exports[`Parse a single number 1`] = ` +Node { + "__id": "node_7", + "body": Array [ + Node { + "__id": "node_6", + "end": 3, + "expression": Node { + "__id": "node_5", + "end": 2, + "loc": SourceLocation { + "end": Position { + "column": 2, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "raw": "42", + "start": 0, + "type": "Literal", + "value": 42, + }, + "loc": SourceLocation { + "end": Position { + "column": 3, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "start": 0, + "type": "ExpressionStatement", + }, + ], + "end": 3, + "loc": SourceLocation { + "end": Position { + "column": 3, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "sourceType": "script", + "start": 0, + "type": "Program", +} +`; + +exports[`Parse a single string 1`] = ` +Node { + "__id": "node_4", + "body": Array [ + Node { + "__id": "node_3", + "directive": "42", + "end": 5, + "expression": Node { + "__id": "node_2", + "end": 4, + "loc": SourceLocation { + "end": Position { + "column": 4, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "raw": "'42'", + "start": 0, + "type": "Literal", + "value": "42", + }, + "loc": SourceLocation { + "end": Position { + "column": 5, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "start": 0, + "type": "ExpressionStatement", + }, + ], + "end": 5, + "loc": SourceLocation { + "end": Position { + "column": 5, + "line": 1, + }, + "start": Position { + "column": 0, + "line": 1, + }, + }, + "sourceType": "script", + "start": 0, + "type": "Program", +} +`; diff --git a/src/slang/__tests__/index.ts b/src/slang/__tests__/index.ts new file mode 100644 index 0000000000..a02e043074 --- /dev/null +++ b/src/slang/__tests__/index.ts @@ -0,0 +1,83 @@ +import { stripIndent } from 'common-tags' +import { mockContext } from '../../mocks/context' +import { parseError, runInContext } from '../index' +import { Finished } from '../types' + +test('Empty code returns undefined', () => { + const code = '' + const context = mockContext() + const promise = runInContext(code, context) + return promise.then(obj => { + expect(obj).toMatchSnapshot() + expect(obj.status).toBe('finished') + expect((obj as Finished).value).toBe(undefined) + }) +}) + +test('Single string self-evaluates to itself', () => { + const code = "'42';" + const context = mockContext() + const promise = runInContext(code, context) + return promise.then(obj => { + expect(obj).toMatchSnapshot() + expect(obj.status).toBe('finished') + expect((obj as Finished).value).toBe('42') + }) +}) + +test('Single number self-evaluates to itself', () => { + const code = '42;' + const context = mockContext() + const promise = runInContext(code, context) + return promise.then(obj => { + expect(obj).toMatchSnapshot() + expect(obj.status).toBe('finished') + expect((obj as Finished).value).toBe(42) + }) +}) + +test('Single boolean self-evaluates to itself', () => { + const code = 'true;' + const context = mockContext() + const promise = runInContext(code, context) + return promise.then(obj => { + expect(obj).toMatchSnapshot() + expect(obj.status).toBe('finished') + expect((obj as Finished).value).toBe(true) + }) +}) + +test('Arrow function definition returns itself', () => { + const code = '() => 42;' + const context = mockContext() + const promise = runInContext(code, context) + return promise.then(obj => { + expect(obj).toMatchSnapshot() + expect(obj.status).toBe('finished') + expect((obj as Finished).value).toMatchSnapshot() + }) +}) + +test('Factorial arrow function', () => { + const code = stripIndent` + const fac = (i) => i === 1 ? 1 : i * fac(i-1); + fac(5); + ` + const context = mockContext() + const promise = runInContext(code, context) + return promise.then(obj => { + expect(obj).toMatchSnapshot() + expect(obj.status).toBe('finished') + expect((obj as Finished).value).toBe(120) + }) +}) + +test('parseError for missing semicolon', () => { + const code = '42' + const context = mockContext() + const promise = runInContext(code, context) + return promise.then(obj => { + const errors = parseError(context.errors) + expect(errors).toMatchSnapshot() + }) +}) diff --git a/src/slang/__tests__/parser.ts b/src/slang/__tests__/parser.ts new file mode 100644 index 0000000000..e9f1562486 --- /dev/null +++ b/src/slang/__tests__/parser.ts @@ -0,0 +1,20 @@ +import { mockContext } from '../../mocks/context' +import { parse } from '../parser' + +test('Empty parse returns empty Program Node', () => { + const context = mockContext() + const program = parse('', context) + expect(program).toMatchSnapshot() +}) + +test('Parse a single string', () => { + const context = mockContext() + const program = parse("'42';", context) + expect(program).toMatchSnapshot() +}) + +test('Parse a single number', () => { + const context = mockContext() + const program = parse('42;', context) + expect(program).toMatchSnapshot() +}) diff --git a/src/slang/cfg.ts b/src/slang/cfg.ts new file mode 100644 index 0000000000..9e30e8ce40 --- /dev/null +++ b/src/slang/cfg.ts @@ -0,0 +1,148 @@ +import { base, recursive, Walker, Walkers } from 'acorn/dist/walk' +import * as es from 'estree' +import * as invariant from 'invariant' + +import { Types } from './constants' +import { CFG, Context } from './types' +import { composeWalker } from './utils/node' + +const freshLambda = (() => { + let id = 0 + return () => { + id++ + return 'lambda_' + id + } +})() + +const walkers: Walkers<{}> = {} + +let nodeStack: es.Node[] = [] +let scopeQueue: CFG.Scope[] = [] +let edgeLabel: CFG.EdgeLabel = 'next' + +const currentScope = () => scopeQueue[0]! + +const connect = (node: es.Node, context: Context) => { + // Empty node stack, connect all of them with node + let lastNode = nodeStack.pop() + const vertex = context.cfg.nodes[node.__id!] + + // If there is no last node, this is the first node in the scope. + if (!lastNode) { + currentScope().entry = vertex + } + while (lastNode) { + // Connect previously visited node with this node + context.cfg.edges[lastNode.__id!].push({ + type: edgeLabel, + to: vertex + }) + lastNode = nodeStack.pop() + } + // Reset edge label + edgeLabel = 'next' + nodeStack.push(node) +} + +const exitScope = (context: Context) => { + while (nodeStack.length > 0) { + const node = nodeStack.shift()! + const vertex = context.cfg.nodes[node.__id!] + currentScope().exits.push(vertex) + } +} + +walkers.ExpressionStatement = composeWalker(connect, base.ExpressionStatement) +walkers.VariableDeclaration = composeWalker(connect, base.VariableDeclaration) + +const walkIfStatement: Walker = (node, context, recurse) => { + const test = node.test as es.Node + let consequentExit + let alternateExit + // Connect test with previous node + connect(test, context) + + // Process the consequent branch + edgeLabel = 'consequent' + recurse(node.consequent, context) + // Remember exits from consequent + consequentExit = nodeStack + // Process the alternate branch + if (node.alternate) { + const alternate = node.alternate + edgeLabel = 'alternate' + nodeStack = [test] + recurse(alternate, context) + alternateExit = nodeStack + } + // Restore node Stack to consequent exits + nodeStack = consequentExit + // Add alternate exits if any + if (alternateExit) { + nodeStack = nodeStack.concat(alternateExit) + } +} +walkers.IfStatement = walkIfStatement + +const walkReturnStatement: Walker = (node, state) => { + connect(node, state) + exitScope(state) +} +walkers.ReturnStatement = composeWalker(base.ReturnStatement, walkReturnStatement) + +const walkFunction: Walker = ( + node, + context, + recurse +) => { + // Check whether function declaration is from outer scope or its own + if (scopeQueue[0].node !== node) { + connect(node, context) + const name = node.id ? node.id.name : freshLambda() + const scope: CFG.Scope = { + name, + type: Types.ANY, + node, + parent: currentScope(), + exits: [], + env: {} + } + scopeQueue.push(scope!) + context.cfg.scopes.push(scope) + } else { + node.body.body.forEach(child => { + recurse(child, context) + }) + exitScope(context) + } +} +walkers.FunctionDeclaration = walkers.FunctionExpression = walkFunction + +const walkProgram: Walker = (node, context, recurse) => { + exitScope(context) +} +walkers.Program = composeWalker(base.Program, walkProgram) + +export const generateCFG = (context: Context) => { + invariant( + context.cfg.scopes.length >= 1, + `context.cfg.scopes must contain + exactly the global scope before generating CFG` + ) + invariant( + context.cfg.scopes[0].node, + `context.cfg.scopes[0] node + must be a program from the parser` + ) + // Reset states + nodeStack = [] + scopeQueue = [context.cfg.scopes[0]] + edgeLabel = 'next' + + // Process Node BFS style + while (scopeQueue.length > 0) { + const current = scopeQueue[0].node! + recursive(current, context, walkers) + scopeQueue.shift() + } +} diff --git a/src/slang/constants.ts b/src/slang/constants.ts new file mode 100644 index 0000000000..67fa07955f --- /dev/null +++ b/src/slang/constants.ts @@ -0,0 +1,21 @@ +import * as es from 'estree' +import { CFG } from './types' + +export const Types = { + NUMBER: { name: 'number' } as CFG.Type, + STRING: { name: 'string' } as CFG.Type, + UNDEFINED: { name: 'undefined' } as CFG.Type, + BOOLEAN: { name: 'boolean' } as CFG.Type, + ANY: { name: 'any' } as CFG.Type +} +export const MAX_LIST_DISPLAY_LENGTH = 100 +export const UNKNOWN_LOCATION: es.SourceLocation = { + start: { + line: -1, + column: -1 + }, + end: { + line: -1, + column: -1 + } +} diff --git a/src/slang/createContext.ts b/src/slang/createContext.ts new file mode 100644 index 0000000000..45921567b0 --- /dev/null +++ b/src/slang/createContext.ts @@ -0,0 +1,145 @@ +import * as list from './stdlib/list' +import * as misc from './stdlib/misc' +import { Context, Value } from './types' + +const GLOBAL = typeof window === 'undefined' ? global : window + +const createEmptyCFG = () => ({ + nodes: {}, + edges: {}, + scopes: [] +}) + +const createEmptyRuntime = () => ({ + isRunning: false, + frames: [], + value: undefined, + nodes: [] +}) + +export const createEmptyContext = (week: number): Context => ({ + week, + errors: [], + cfg: createEmptyCFG(), + runtime: createEmptyRuntime() +}) + +export const ensureGlobalEnvironmentExist = (context: Context) => { + if (!context.runtime) { + context.runtime = createEmptyRuntime() + } + if (!context.runtime.frames) { + context.runtime.frames = [] + } + if (context.runtime.frames.length === 0) { + context.runtime.frames.push({ + parent: null, + name: 'global', + environment: {} + }) + } +} + +const defineSymbol = (context: Context, name: string, value: Value) => { + const globalFrame = context.runtime.frames[0] + globalFrame.environment[name] = value +} + +export const importExternals = (context: Context, externals: string[]) => { + ensureGlobalEnvironmentExist(context) + + externals.forEach(symbol => { + defineSymbol(context, symbol, GLOBAL[symbol]) + }) +} + +export const importBuiltins = (context: Context) => { + ensureGlobalEnvironmentExist(context) + + if (context.week >= 3) { + defineSymbol(context, 'math_PI', Math.PI) + defineSymbol(context, 'math_sqrt', Math.sqrt) + defineSymbol(context, 'runtime', misc.runtime) + defineSymbol(context, 'display', misc.display) + defineSymbol(context, 'error', misc.error_message) + defineSymbol(context, 'prompt', prompt) + defineSymbol(context, 'parse_int', misc.parse_int) + defineSymbol(context, 'undefined', undefined) + } + + if (context.week >= 4) { + defineSymbol(context, 'math_log', Math.log) + defineSymbol(context, 'math_exp', Math.exp) + defineSymbol(context, 'alert', alert) + defineSymbol(context, 'math_floor', Math.floor) + defineSymbol(context, 'timed', misc.timed) + + // Define all Math libraries + const objs = Object.getOwnPropertyNames(Math) + for (const i in objs) { + if (objs.hasOwnProperty(i)) { + const val = objs[i] + if (typeof Math[val] === 'function') { + defineSymbol(context, 'math_' + val, Math[val].bind()) + } else { + defineSymbol(context, 'math_' + val, Math[val]) + } + } + } + } + + if (context.week >= 5) { + defineSymbol(context, 'list', list.list) + defineSymbol(context, 'pair', list.pair) + defineSymbol(context, 'is_pair', list.is_pair) + defineSymbol(context, 'is_list', list.is_list) + defineSymbol(context, 'is_empty_list', list.is_empty_list) + defineSymbol(context, 'head', list.head) + defineSymbol(context, 'tail', list.tail) + defineSymbol(context, 'length', list.length) + defineSymbol(context, 'map', list.map) + defineSymbol(context, 'build_list', list.build_list) + defineSymbol(context, 'for_each', list.for_each) + defineSymbol(context, 'list_to_string', list.list_to_string) + defineSymbol(context, 'reverse', list.reverse) + defineSymbol(context, 'append', list.append) + defineSymbol(context, 'member', list.member) + defineSymbol(context, 'remove', list.remove) + defineSymbol(context, 'remove_all', list.remove_all) + defineSymbol(context, 'equal', list.equal) + defineSymbol(context, 'assoc', list.assoc) + defineSymbol(context, 'filter', list.filter) + defineSymbol(context, 'enum_list', list.enum_list) + defineSymbol(context, 'list_ref', list.list_ref) + defineSymbol(context, 'accumulate', list.accumulate) + if (window.hasOwnProperty('ListVisualizer')) { + defineSymbol(context, 'draw', (window as any).ListVisualizer.draw) + } else { + defineSymbol(context, 'draw', () => { + throw new Error('List visualizer is not enabled') + }) + } + } + if (context.week >= 6) { + defineSymbol(context, 'is_number', misc.is_number) + } + if (context.week >= 8) { + defineSymbol(context, 'undefined', undefined) + defineSymbol(context, 'set_head', list.set_head) + defineSymbol(context, 'set_tail', list.set_tail) + } + if (context.week >= 9) { + defineSymbol(context, 'array_length', misc.array_length) + } +} + +const createContext = (week = 3, externals = []) => { + const context = createEmptyContext(week) + + importBuiltins(context) + importExternals(context, externals) + + return context +} + +export default createContext diff --git a/src/slang/index.ts b/src/slang/index.ts new file mode 100644 index 0000000000..ea94e2c3aa --- /dev/null +++ b/src/slang/index.ts @@ -0,0 +1,65 @@ +import createContext from './createContext' +import { toString } from './interop' +import { evaluate } from './interpreter' +import { InterruptedError } from './interpreter-errors' +import { parse } from './parser' +import { AsyncScheduler, PreemptiveScheduler } from './schedulers' +import { Context, Result, Scheduler, SourceError } from './types' + +export interface IOptions { + scheduler: 'preemptive' | 'async' + steps: number +} + +const DEFAULT_OPTIONS: IOptions = { + scheduler: 'async', + steps: 1000 +} + +export function parseError(errors: SourceError[]): string { + const errorMessagesArr = errors.map(error => { + const line = error.location ? error.location.start.line : '' + const explanation = error.explain() + return `Line ${line}: ${explanation}` + }) + return errorMessagesArr.join('\n') +} + +export function runInContext( + code: string, + context: Context, + options: Partial = {} +): Promise { + const theOptions: IOptions = { ...options, ...DEFAULT_OPTIONS } + context.errors = [] + const program = parse(code, context) + if (program) { + const it = evaluate(program, context) + let scheduler: Scheduler + if (options.scheduler === 'async') { + scheduler = new AsyncScheduler() + } else { + scheduler = new PreemptiveScheduler(theOptions.steps) + } + return scheduler.run(it, context) + } else { + return Promise.resolve({ status: 'error' } as Result) + } +} + +export function resume(result: Result) { + if (result.status === 'finished' || result.status === 'error') { + return result + } else { + return result.scheduler.run(result.it, result.context) + } +} + +export function interrupt(context: Context) { + const globalFrame = context.runtime.frames[context.runtime.frames.length - 1] + context.runtime.frames = [globalFrame] + context.runtime.isRunning = false + context.errors.push(new InterruptedError(context.runtime.nodes[0])) +} + +export { createContext, toString, Context, Result } diff --git a/src/slang/interop.ts b/src/slang/interop.ts new file mode 100644 index 0000000000..082b8a6985 --- /dev/null +++ b/src/slang/interop.ts @@ -0,0 +1,82 @@ +import { generate } from 'astring' +import { MAX_LIST_DISPLAY_LENGTH } from './constants' +import { apply } from './interpreter' +import { Closure, Context, Value } from './types' + +export const closureToJS = (value: Value, context: Context, klass: string) => { + function DummyClass(this: Value) { + const args: Value[] = Array.prototype.slice.call(arguments) + const gen = apply(context, value, args, undefined, this) + let it = gen.next() + while (!it.done) { + it = gen.next() + } + return it.value + } + Object.defineProperty(DummyClass, 'name', { + value: klass + }) + Object.setPrototypeOf(DummyClass, () => {}) + Object.defineProperty(DummyClass, 'Inherits', { + value: (Parent: Value) => { + DummyClass.prototype = Object.create(Parent.prototype) + DummyClass.prototype.constructor = DummyClass + } + }) + DummyClass.call = (thisArg: Value, ...args: Value[]) => { + return DummyClass.apply(thisArg, args) + } + return DummyClass +} + +export const toJS = (value: Value, context: Context, klass?: string) => { + if (value instanceof Closure) { + return value.fun + } else { + return value + } +} + +const stripBody = (body: string) => { + const lines = body.split(/\n/) + if (lines.length >= 2) { + return lines[0] + '\n\t[implementation hidden]\n' + lines[lines.length - 1] + } else { + return body + } +} + +const arrayToString = (value: Value[], length: number) => { + // Normal Array + if (value.length > 2 || value.length === 1) { + return `[${value}]` + } else if (value.length === 0) { + return '[]' + } else { + return `[${toString(value[0], length + 1)}, ${toString(value[1], length + 1)}]` + } +} + +export const toString = (value: Value, length = 0): string => { + if (value instanceof Closure) { + return generate(value.node) + } else if (Array.isArray(value)) { + if (length > MAX_LIST_DISPLAY_LENGTH) { + return '...' + } else { + return arrayToString(value, length) + } + } else if (typeof value === 'string') { + return `\"${value}\"` + } else if (typeof value === 'undefined') { + return 'undefined' + } else if (typeof value === 'function') { + if (value.__SOURCE__) { + return `function ${value.__SOURCE__} {\n\t[implementation hidden]\n}` + } else { + return stripBody(value.toString()) + } + } else { + return value.toString() + } +} diff --git a/src/slang/interpreter-errors.ts b/src/slang/interpreter-errors.ts new file mode 100644 index 0000000000..dd802dfd74 --- /dev/null +++ b/src/slang/interpreter-errors.ts @@ -0,0 +1,136 @@ +/* tslint:disable: max-classes-per-file */ +import { generate } from 'astring' +import * as es from 'estree' +import { UNKNOWN_LOCATION } from './constants' +import { toString } from './interop' +import { ErrorSeverity, ErrorType, SourceError, Value } from './types' + +export class InterruptedError implements SourceError { + public type = ErrorType.RUNTIME + public severity = ErrorSeverity.ERROR + public location: es.SourceLocation + + constructor(node: es.Node) { + this.location = node.loc! + } + + public explain() { + return 'Execution aborted by user.' + } + + public elaborate() { + return 'TODO' + } +} + +export class ExceptionError implements SourceError { + public type = ErrorType.RUNTIME + public severity = ErrorSeverity.ERROR + + constructor(public error: Error, public location: es.SourceLocation) {} + + public explain() { + return this.error.toString() + } + + public elaborate() { + return 'TODO' + } +} + +export class MaximumStackLimitExceeded implements SourceError { + public type = ErrorType.RUNTIME + public severity = ErrorSeverity.ERROR + public location: es.SourceLocation + + constructor(node: es.Node, private calls: es.CallExpression[]) { + this.location = node ? node.loc! : UNKNOWN_LOCATION + } + + public explain() { + return ` + Infinite recursion + ${generate(this.calls[0])}..${generate(this.calls[1])}..${generate(this.calls[2])}.. + ` + } + + public elaborate() { + return 'TODO' + } +} + +export class CallingNonFunctionValue implements SourceError { + public type = ErrorType.RUNTIME + public severity = ErrorSeverity.ERROR + public location: es.SourceLocation + + constructor(private callee: Value, node?: es.Node) { + if (node) { + this.location = node.loc! + } else { + this.location = UNKNOWN_LOCATION + } + } + + public explain() { + return `Calling non-function value ${toString(this.callee)}` + } + + public elaborate() { + return 'TODO' + } +} + +export class UndefinedVariable implements SourceError { + public type = ErrorType.RUNTIME + public severity = ErrorSeverity.ERROR + public location: es.SourceLocation + + constructor(public name: string, node: es.Node) { + this.location = node.loc! + } + + public explain() { + return `Undefined Variable ${this.name}` + } + + public elaborate() { + return 'TODO' + } +} + +export class InvalidNumberOfArguments implements SourceError { + public type = ErrorType.RUNTIME + public severity = ErrorSeverity.ERROR + public location: es.SourceLocation + + constructor(node: es.Node, private expected: number, private got: number) { + this.location = node.loc! + } + + public explain() { + return `Expected ${this.expected} arguments, but got ${this.got}` + } + + public elaborate() { + return 'TODO' + } +} + +export class VariableRedeclaration implements SourceError { + public type = ErrorType.RUNTIME + public severity = ErrorSeverity.ERROR + public location: es.SourceLocation + + constructor(node: es.Node, private name: string) { + this.location = node.loc! + } + + public explain() { + return `Redeclaring variable ${this.name}` + } + + public elaborate() { + return 'TODO' + } +} diff --git a/src/slang/interpreter.ts b/src/slang/interpreter.ts new file mode 100644 index 0000000000..6771b878c0 --- /dev/null +++ b/src/slang/interpreter.ts @@ -0,0 +1,486 @@ +/* tslint:disable: max-classes-per-file */ +import * as es from 'estree' +import * as constants from './constants' +import { toJS } from './interop' +import * as errors from './interpreter-errors' +import { ArrowClosure, Closure, Context, ErrorSeverity, Frame, SourceError, Value } from './types' +import { createNode } from './utils/node' +import * as rttc from './utils/rttc' + +class ReturnValue { + constructor(public value: Value) {} +} + +class BreakValue {} + +class ContinueValue {} + +class TailCallReturnValue { + constructor(public callee: Closure, public args: Value[], public node: es.CallExpression) {} +} + +const createFrame = ( + closure: ArrowClosure | Closure, + args: Value[], + callExpression?: es.CallExpression +): Frame => { + const frame: Frame = { + name: closure.name, // TODO: Change this + parent: closure.frame, + environment: {} + } + if (callExpression) { + frame.callExpression = { + ...callExpression, + arguments: args.map(a => createNode(a) as es.Expression) + } + } + closure.node.params.forEach((param, index) => { + const ident = param as es.Identifier + frame.environment[ident.name] = args[index] + }) + return frame +} + +const handleError = (context: Context, error: SourceError) => { + context.errors.push(error) + if (error.severity === ErrorSeverity.ERROR) { + const globalFrame = context.runtime.frames[context.runtime.frames.length - 1] + context.runtime.frames = [globalFrame] + throw error + } else { + return context + } +} + +function defineVariable(context: Context, name: string, value: Value) { + const frame = context.runtime.frames[0] + + if (frame.environment.hasOwnProperty(name)) { + handleError(context, new errors.VariableRedeclaration(context.runtime.nodes[0]!, name)) + } + + frame.environment[name] = value + + return frame +} +function* visit(context: Context, node: es.Node) { + context.runtime.nodes.unshift(node) + yield context +} +function* leave(context: Context) { + context.runtime.nodes.shift() + yield context +} +const currentFrame = (context: Context) => context.runtime.frames[0] +const replaceFrame = (context: Context, frame: Frame) => (context.runtime.frames[0] = frame) +const popFrame = (context: Context) => context.runtime.frames.shift() +const pushFrame = (context: Context, frame: Frame) => context.runtime.frames.unshift(frame) + +const getVariable = (context: Context, name: string) => { + let frame: Frame | null = context.runtime.frames[0] + while (frame) { + if (frame.environment.hasOwnProperty(name)) { + return frame.environment[name] + } else { + frame = frame.parent + } + } + handleError(context, new errors.UndefinedVariable(name, context.runtime.nodes[0])) +} + +const setVariable = (context: Context, name: string, value: any) => { + let frame: Frame | null = context.runtime.frames[0] + while (frame) { + if (frame.environment.hasOwnProperty(name)) { + frame.environment[name] = value + return + } else { + frame = frame.parent + } + } + handleError(context, new errors.UndefinedVariable(name, context.runtime.nodes[0])) +} + +const checkNumberOfArguments = ( + context: Context, + callee: ArrowClosure | Closure, + args: Value[], + exp: es.CallExpression +) => { + if (callee.node.params.length !== args.length) { + const error = new errors.InvalidNumberOfArguments(exp, callee.node.params.length, args.length) + handleError(context, error) + } +} + +function* getArgs(context: Context, call: es.CallExpression) { + const args = [] + for (const arg of call.arguments) { + args.push(yield* evaluate(arg, context)) + } + return args +} + +export type Evaluator = (node: T, context: Context) => IterableIterator + +export const evaluators: { [nodeType: string]: Evaluator } = { + /** Simple Values */ + *Literal(node: es.Literal, context: Context) { + return node.value + }, + *ThisExpression(node: es.ThisExpression, context: Context) { + return context.runtime.frames[0].thisContext + }, + *ArrayExpression(node: es.ArrayExpression, context: Context) { + const res = [] + for (const n of node.elements) { + res.push(yield* evaluate(n, context)) + } + return res + }, + *FunctionExpression(node: es.FunctionExpression, context: Context) { + return new Closure(node, currentFrame(context), context) + }, + *ArrowFunctionExpression(node: es.Function, context: Context) { + return new ArrowClosure(node, currentFrame(context), context) + }, + *Identifier(node: es.Identifier, context: Context) { + return getVariable(context, node.name) + }, + *CallExpression(node: es.CallExpression, context: Context) { + const callee = yield* evaluate(node.callee, context) + const args = yield* getArgs(context, node) + let thisContext + if (node.callee.type === 'MemberExpression') { + thisContext = yield* evaluate(node.callee.object, context) + } + const result = yield* apply(context, callee, args, node, thisContext) + return result + }, + *NewExpression(node: es.NewExpression, context: Context) { + const callee = yield* evaluate(node.callee, context) + const args = [] + for (const arg of node.arguments) { + args.push(yield* evaluate(arg, context)) + } + const obj: Value = {} + if (callee instanceof Closure) { + obj.__proto__ = callee.fun.prototype + callee.fun.apply(obj, args) + } else { + obj.__proto__ = callee.prototype + callee.apply(obj, args) + } + return obj + }, + *UnaryExpression(node: es.UnaryExpression, context: Context) { + const value = yield* evaluate(node.argument, context) + if (node.operator === '!') { + return !value + } else if (node.operator === '-') { + return -value + } else { + return +value + } + }, + *BinaryExpression(node: es.BinaryExpression, context: Context) { + const left = yield* evaluate(node.left, context) + const right = yield* evaluate(node.right, context) + + rttc.checkBinaryExpression(context, node.operator, left, right) + + let result + switch (node.operator) { + case '+': + result = left + right + break + case '-': + result = left - right + break + case '*': + result = left * right + break + case '/': + result = left / right + break + case '%': + result = left % right + break + case '===': + result = left === right + break + case '!==': + result = left !== right + break + case '<=': + result = left <= right + break + case '<': + result = left < right + break + case '>': + result = left > right + break + case '>=': + result = left >= right + break + default: + result = undefined + } + return result + }, + *ConditionalExpression(node: es.ConditionalExpression, context: Context) { + return yield* this.IfStatement(node, context) + }, + *LogicalExpression(node: es.LogicalExpression, context: Context) { + const left = yield* evaluate(node.left, context) + if ((node.operator === '&&' && left) || (node.operator === '||' && !left)) { + return yield* evaluate(node.right, context) + } else { + return left + } + }, + *VariableDeclaration(node: es.VariableDeclaration, context: Context) { + const declaration = node.declarations[0] + const id = declaration.id as es.Identifier + const value = yield* evaluate(declaration.init!, context) + defineVariable(context, id.name, value) + return undefined + }, + *ContinueStatement(node: es.ContinueStatement, context: Context) { + return new ContinueValue() + }, + *BreakStatement(node: es.BreakStatement, context: Context) { + return new BreakValue() + }, + *ForStatement(node: es.ForStatement, context: Context) { + if (node.init) { + yield* evaluate(node.init, context) + } + let test = node.test ? yield* evaluate(node.test, context) : true + let value + while (test) { + value = yield* evaluate(node.body, context) + if (value instanceof ContinueValue) { + value = undefined + } + if (value instanceof BreakValue) { + value = undefined + break + } + if (value instanceof ReturnValue) { + break + } + if (node.update) { + yield* evaluate(node.update, context) + } + test = node.test ? yield* evaluate(node.test, context) : true + } + if (value instanceof BreakValue) { + return undefined + } + return value + }, + *MemberExpression(node: es.MemberExpression, context: Context) { + let obj = yield* evaluate(node.object, context) + if (obj instanceof Closure) { + obj = obj.fun + } + if (node.computed) { + const prop = yield* evaluate(node.property, context) + return obj[prop] + } else { + const name = (node.property as es.Identifier).name + if (name === 'prototype') { + return obj.prototype + } else { + return obj[name] + } + } + }, + *AssignmentExpression(node: es.AssignmentExpression, context: Context) { + if (node.left.type === 'MemberExpression') { + const left = node.left + const obj = yield* evaluate(left.object, context) + let prop + if (left.computed) { + prop = yield* evaluate(left.property, context) + } else { + prop = (left.property as es.Identifier).name + } + const val = yield* evaluate(node.right, context) + obj[prop] = val + return val + } + const id = node.left as es.Identifier + // Make sure it exist + const value = yield* evaluate(node.right, context) + setVariable(context, id.name, value) + return value + }, + *FunctionDeclaration(node: es.FunctionDeclaration, context: Context) { + const id = node.id as es.Identifier + // tslint:disable-next-line:no-any + const closure = new Closure(node as any, currentFrame(context), context) + defineVariable(context, id.name, closure) + return undefined + }, + *IfStatement(node: es.IfStatement, context: Context) { + const test = yield* evaluate(node.test, context) + if (test) { + return yield* evaluate(node.consequent, context) + } else if (node.alternate) { + return yield* evaluate(node.alternate, context) + } else { + return undefined + } + }, + *ExpressionStatement(node: es.ExpressionStatement, context: Context) { + return yield* evaluate(node.expression, context) + }, + *ReturnStatement(node: es.ReturnStatement, context: Context) { + if (node.argument) { + if (node.argument.type === 'CallExpression') { + const callee = yield* evaluate(node.argument.callee, context) + const args = yield* getArgs(context, node.argument) + return new TailCallReturnValue(callee, args, node.argument) + } else { + return new ReturnValue(yield* evaluate(node.argument, context)) + } + } else { + return new ReturnValue(undefined) + } + }, + *WhileStatement(node: es.WhileStatement, context: Context) { + let value: any // tslint:disable-line + let test + while ( + // tslint:disable-next-line + (test = yield* evaluate(node.test, context)) && + !(value instanceof ReturnValue) && + !(value instanceof BreakValue) && + !(value instanceof TailCallReturnValue) + ) { + value = yield* evaluate(node.body, context) + } + if (value instanceof BreakValue) { + return undefined + } + return value + }, + *ObjectExpression(node: es.ObjectExpression, context: Context) { + const obj = {} + for (const prop of node.properties) { + let key + if (prop.key.type === 'Identifier') { + key = prop.key.name + } else { + key = yield* evaluate(prop.key, context) + } + obj[key] = yield* evaluate(prop.value, context) + } + return obj + }, + *BlockStatement(node: es.BlockStatement, context: Context) { + let result: Value + for (const statement of node.body) { + result = yield* evaluate(statement, context) + if ( + result instanceof ReturnValue || + result instanceof BreakValue || + result instanceof ContinueValue + ) { + break + } + } + return result + }, + *Program(node: es.BlockStatement, context: Context) { + let result: Value + for (const statement of node.body) { + result = yield* evaluate(statement, context) + if (result instanceof ReturnValue) { + break + } + } + return result + } +} + +export function* evaluate(node: es.Node, context: Context) { + yield* visit(context, node) + const result = yield* evaluators[node.type](node, context) + yield* leave(context) + return result +} + +export function* apply( + context: Context, + fun: ArrowClosure | Closure | Value, + args: Value[], + node?: es.CallExpression, + thisContext?: Value +) { + let result: Value + let total = 0 + + while (!(result instanceof ReturnValue)) { + if (fun instanceof Closure) { + checkNumberOfArguments(context, fun, args, node!) + const frame = createFrame(fun, args, node) + frame.thisContext = thisContext + if (result instanceof TailCallReturnValue) { + replaceFrame(context, frame) + } else { + pushFrame(context, frame) + total++ + } + result = yield* evaluate(fun.node.body, context) + if (result instanceof TailCallReturnValue) { + fun = result.callee + node = result.node + args = result.args + } else if (!(result instanceof ReturnValue)) { + // No Return Value, set it as undefined + result = new ReturnValue(undefined) + } + } else if (fun instanceof ArrowClosure) { + checkNumberOfArguments(context, fun, args, node!) + const frame = createFrame(fun, args, node) + frame.thisContext = thisContext + if (result instanceof TailCallReturnValue) { + replaceFrame(context, frame) + } else { + pushFrame(context, frame) + total++ + } + result = new ReturnValue(yield* evaluate(fun.node.body, context)) + } else if (typeof fun === 'function') { + try { + const as = args.map(a => toJS(a, context)) + result = fun.apply(thisContext, as) + break + } catch (e) { + // Recover from exception + const globalFrame = context.runtime.frames[context.runtime.frames.length - 1] + context.runtime.frames = [globalFrame] + const loc = node ? node.loc! : constants.UNKNOWN_LOCATION + handleError(context, new errors.ExceptionError(e, loc)) + result = undefined + } + } else { + handleError(context, new errors.CallingNonFunctionValue(fun, node)) + result = undefined + break + } + } + // Unwraps return value and release stack frame + if (result instanceof ReturnValue) { + result = result.value + } + for (let i = 1; i <= total; i++) { + popFrame(context) + } + return result +} diff --git a/src/slang/parser.ts b/src/slang/parser.ts new file mode 100644 index 0000000000..e975b8c564 --- /dev/null +++ b/src/slang/parser.ts @@ -0,0 +1,205 @@ +/* tslint:disable: max-classes-per-file */ +import { Options as AcornOptions, parse as acornParse, Position } from 'acorn' +import { simple } from 'acorn/dist/walk' +import { stripIndent } from 'common-tags' +import * as es from 'estree' + +import rules from './rules' +import syntaxTypes from './syntaxTypes' +import { Context, ErrorSeverity, ErrorType, SourceError } from './types' + +// tslint:disable-next-line:interface-name +export interface ParserOptions { + week: number +} + +export class DisallowedConstructError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + public nodeType: string + + constructor(public node: es.Node) { + this.nodeType = this.splitNodeType() + } + + get location() { + return this.node.loc! + } + + public explain() { + return `${this.nodeType} is not allowed` + } + + public elaborate() { + return stripIndent` + You are trying to use ${this.nodeType}, which is not yet allowed (yet). + ` + } + + private splitNodeType() { + const nodeType = this.node.type + const tokens: string[] = [] + let soFar = '' + for (let i = 0; i < nodeType.length; i++) { + const isUppercase = nodeType[i] === nodeType[i].toUpperCase() + if (isUppercase && i > 0) { + tokens.push(soFar) + soFar = '' + } else { + soFar += nodeType[i] + } + } + return tokens.join(' ') + } +} + +export class FatalSyntaxError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + public constructor(public location: es.SourceLocation, public message: string) {} + + public explain() { + return this.message + } + + public elaborate() { + return 'There is a syntax error in your program' + } +} + +export class MissingSemicolonError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + public constructor(public location: es.SourceLocation) {} + + public explain() { + return 'Missing semicolon at the end of statement' + } + + public elaborate() { + return 'Every statement must be terminated by a semicolon.' + } +} + +export class TrailingCommaError implements SourceError { + public type: ErrorType.SYNTAX + public severity: ErrorSeverity.WARNING + public constructor(public location: es.SourceLocation) {} + + public explain() { + return 'Trailing comma' + } + + public elaborate() { + return 'Please remove the trailing comma' + } +} + +export const freshId = (() => { + let id = 0 + return () => { + id++ + return 'node_' + id + } +})() + +function compose( + w1: (node: T, state: S) => void, + w2: (node: T, state: S) => void +) { + return (node: T, state: S) => { + w1(node, state) + w2(node, state) + } +} + +const walkers: { + [name: string]: (node: es.Node, context: Context) => void +} = {} + +for (const type of Object.keys(syntaxTypes)) { + walkers[type] = (node: es.Node, context: Context) => { + const id = freshId() + Object.defineProperty(node, '__id', { + enumerable: true, + configurable: false, + writable: false, + value: id + }) + context.cfg.nodes[id] = { + id, + node, + scope: undefined, + usages: [] + } + context.cfg.edges[id] = [] + if (syntaxTypes[node.type] > context.week) { + context.errors.push(new DisallowedConstructError(node)) + } + } +} + +const createAcornParserOptions = (context: Context): AcornOptions => ({ + sourceType: 'script', + ecmaVersion: 6, + locations: true, + // tslint:disable-next-line:no-any + onInsertedSemicolon(end: any, loc: any) { + context.errors.push( + new MissingSemicolonError({ + end: { line: loc.line, column: loc.column + 1 }, + start: loc + }) + ) + }, + // tslint:disable-next-line:no-any + onTrailingComma(end: any, loc: Position) { + context.errors.push( + new TrailingCommaError({ + end: { line: loc.line, column: loc.column + 1 }, + start: loc + }) + ) + } +}) + +rules.forEach(rule => { + const keys = Object.keys(rule.checkers) + keys.forEach(key => { + walkers[key] = compose(walkers[key], (node, context) => { + if (typeof rule.disableOn !== 'undefined' && context.week >= rule.disableOn) { + return + } + const checker = rule.checkers[key] + const errors = checker(node) + errors.forEach(e => context.errors.push(e)) + }) + }) +}) + +export const parse = (source: string, context: Context) => { + let program: es.Program | undefined + try { + program = acornParse(source, createAcornParserOptions(context)) + simple(program, walkers, undefined, context) + } catch (error) { + if (error instanceof SyntaxError) { + // tslint:disable-next-line:no-any + const loc = (error as any).loc + const location = { + start: { line: loc.line, column: loc.column }, + end: { line: loc.line, column: loc.column + 1 } + } + context.errors.push(new FatalSyntaxError(location, error.toString())) + } else { + throw error + } + } + const hasErrors = context.errors.find(m => m.severity === ErrorSeverity.ERROR) + if (program && !hasErrors) { + // context.cfg.scopes[0].node = program + return program + } else { + return undefined + } +} diff --git a/src/slang/rules/bracesAroundIfElse.ts b/src/slang/rules/bracesAroundIfElse.ts new file mode 100644 index 0000000000..37c5b116f5 --- /dev/null +++ b/src/slang/rules/bracesAroundIfElse.ts @@ -0,0 +1,92 @@ +import { generate } from 'astring' +import { stripIndent } from 'common-tags' +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class BracesAroundIfElseError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.IfStatement, private branch: 'consequent' | 'alternate') {} + + get location() { + return this.node.loc! + } + + public explain() { + if (this.branch === 'consequent') { + return 'Missing curly braces around "if" block' + } else { + return 'Missing curly braces around "else" block' + } + } + + public elaborate() { + let ifOrElse + let header + let body + if (this.branch === 'consequent') { + ifOrElse = 'if' + header = `if (${generate(this.node.test)})` + body = this.node.consequent + } else { + ifOrElse = header = 'else' + body = this.node.alternate + } + + return stripIndent` + ${ifOrElse} block need to be enclosed with a pair of curly braces. + + ${header} { + ${generate(body)} + } + + An exception is when you have an "if" followed by "else if", in this case + "else if" block does not need to be surrounded by curly braces. + + if (someCondition) { + // ... + } else /* notice missing { here */ if (someCondition) { + // ... + } else { + // ... + } + + Rationale: Readability in dense packed code. + + In the snippet below, for instance, with poor indentation it is easy to + mistaken hello() and world() to belong to the same branch of logic. + + if (someCondition) { + 2; + } else + hello(); + world(); + + ` + } +} + +const bracesAroundIfElse: Rule = { + name: 'braces-around-if-else', + + checkers: { + IfStatement(node: es.IfStatement) { + const errors: SourceError[] = [] + if (node.consequent && node.consequent.type !== 'BlockStatement') { + errors.push(new BracesAroundIfElseError(node, 'consequent')) + } + if (node.alternate) { + const notBlock = node.alternate.type !== 'BlockStatement' + const notIf = node.alternate.type !== 'IfStatement' + if (notBlock && notIf) { + errors.push(new BracesAroundIfElseError(node, 'alternate')) + } + } + return errors + } + } +} + +export default bracesAroundIfElse diff --git a/src/slang/rules/bracesAroundWhile.ts b/src/slang/rules/bracesAroundWhile.ts new file mode 100644 index 0000000000..87835839dd --- /dev/null +++ b/src/slang/rules/bracesAroundWhile.ts @@ -0,0 +1,38 @@ +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class BracesAroundWhileError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.WhileStatement) {} + + get location() { + return this.node.loc! + } + + public explain() { + return 'Missing curly braces around "while" block' + } + + public elaborate() { + return 'TODO' + } +} + +const bracesAroundWhile: Rule = { + name: 'braces-around-while', + + checkers: { + WhileStatement(node: es.WhileStatement) { + if (node.body.type !== 'BlockStatement') { + return [new BracesAroundWhileError(node)] + } else { + return [] + } + } + } +} + +export default bracesAroundWhile diff --git a/src/slang/rules/index.ts b/src/slang/rules/index.ts new file mode 100644 index 0000000000..06dae7f47f --- /dev/null +++ b/src/slang/rules/index.ts @@ -0,0 +1,31 @@ +import * as es from 'estree' + +import { Rule } from '../types' + +import bracesAroundIfElse from './bracesAroundIfElse' +import bracesAroundWhile from './bracesAroundWhile' +import noBlockArrowFunction from './noBlockArrowFunction' +import noDeclareMutable from './noDeclareMutable' +import noDeclareReserved from './noDeclareReserved' +import noIfWithoutElse from './noIfWithoutElse' +import noImplicitDeclareUndefined from './noImplicitDeclareUndefined' +import noImplicitReturnUndefined from './noImplicitReturnUndefined' +import noNonEmptyList from './noNonEmptyList' +import singleVariableDeclaration from './singleVariableDeclaration' +import strictEquality from './strictEquality' + +const rules: Array> = [ + bracesAroundIfElse, + bracesAroundWhile, + singleVariableDeclaration, + strictEquality, + noIfWithoutElse, + noImplicitDeclareUndefined, + noImplicitReturnUndefined, + noNonEmptyList, + noBlockArrowFunction, + noDeclareReserved, + noDeclareMutable +] + +export default rules diff --git a/src/slang/rules/noBlockArrowFunction.ts b/src/slang/rules/noBlockArrowFunction.ts new file mode 100644 index 0000000000..cccd6d68d3 --- /dev/null +++ b/src/slang/rules/noBlockArrowFunction.ts @@ -0,0 +1,38 @@ +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class NoBlockArrowFunction implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.ArrowFunctionExpression) {} + + get location() { + return this.node.loc! + } + + public explain() { + return 'Function definition expressions may only end with an expression.' + } + + public elaborate() { + return this.explain() + } +} + +const noBlockArrowFunction: Rule = { + name: 'no-block-arrow-function', + + checkers: { + ArrowFunctionExpression(node: es.ArrowFunctionExpression) { + if (node.body.type === 'BlockStatement') { + return [new NoBlockArrowFunction(node)] + } else { + return [] + } + } + } +} + +export default noBlockArrowFunction diff --git a/src/slang/rules/noDeclareMutable.ts b/src/slang/rules/noDeclareMutable.ts new file mode 100644 index 0000000000..1150548477 --- /dev/null +++ b/src/slang/rules/noDeclareMutable.ts @@ -0,0 +1,42 @@ +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +const mutableDeclarators = ['let', 'var'] + +export class NoDeclareMutableError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.VariableDeclaration) {} + + get location() { + return this.node.loc! + } + + public explain() { + return ( + 'Mutable variable declaration using keyword ' + `'${this.node.kind}'` + ' is not allowed.' + ) + } + + public elaborate() { + return this.explain() + } +} + +const noDeclareMutable: Rule = { + name: 'no-declare-mutable', + + checkers: { + VariableDeclaration(node: es.VariableDeclaration) { + if (mutableDeclarators.includes(node.kind)) { + return [new NoDeclareMutableError(node)] + } else { + return [] + } + } + } +} + +export default noDeclareMutable diff --git a/src/slang/rules/noDeclareReserved.ts b/src/slang/rules/noDeclareReserved.ts new file mode 100644 index 0000000000..22e2b4183a --- /dev/null +++ b/src/slang/rules/noDeclareReserved.ts @@ -0,0 +1,88 @@ +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +const reservedNames = [ + 'break', + 'case', + 'catch', + 'continue', + 'debugger', + 'default', + 'delete', + 'do', + 'else', + 'finally', + 'for', + 'function', + 'if', + 'in', + 'instanceof', + 'new', + 'return', + 'switch', + 'this', + 'throw', + 'try', + 'typeof', + 'var', + 'void', + 'while', + 'with', + 'class', + 'const', + 'enum', + 'export', + 'extends', + 'import', + 'super', + 'implements', + 'interface', + 'let', + 'package', + 'private', + 'protected', + 'public', + 'static', + 'yield', + 'null', + 'true', + 'false' +] + +export class NoDeclareReservedError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.VariableDeclaration) {} + + get location() { + return this.node.loc! + } + + public explain() { + return ( + `Reserved word '${(this.node.declarations[0].id as any).name}'` + ' is not allowed as a name' + ) + } + + public elaborate() { + return this.explain() + } +} + +const noDeclareReserved: Rule = { + name: 'no-declare-reserved', + + checkers: { + VariableDeclaration(node: es.VariableDeclaration) { + if (reservedNames.includes((node.declarations[0].id as any).name)) { + return [new NoDeclareReservedError(node)] + } else { + return [] + } + } + } +} + +export default noDeclareReserved diff --git a/src/slang/rules/noIfWithoutElse.ts b/src/slang/rules/noIfWithoutElse.ts new file mode 100644 index 0000000000..1290f3054c --- /dev/null +++ b/src/slang/rules/noIfWithoutElse.ts @@ -0,0 +1,46 @@ +import { generate } from 'astring' +import { stripIndent } from 'common-tags' +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class NoIfWithoutElseError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.IfStatement) {} + + get location() { + return this.node.loc! + } + + public explain() { + return 'Missing "else" in "if-else" statement' + } + + public elaborate() { + return stripIndent` + This "if" block requires corresponding "else" block which will be + evaluated when ${generate(this.node.test)} expression evaluates to false. + + Later in the course we will lift this restriction and allow "if" without + else. + ` + } +} + +const noIfWithoutElse: Rule = { + name: 'no-if-without-else', + + checkers: { + IfStatement(node: es.IfStatement) { + if (!node.alternate) { + return [new NoIfWithoutElseError(node)] + } else { + return [] + } + } + } +} + +export default noIfWithoutElse diff --git a/src/slang/rules/noImplicitDeclareUndefined.ts b/src/slang/rules/noImplicitDeclareUndefined.ts new file mode 100644 index 0000000000..8c42d495d6 --- /dev/null +++ b/src/slang/rules/noImplicitDeclareUndefined.ts @@ -0,0 +1,48 @@ +import { stripIndent } from 'common-tags' +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class NoImplicitDeclareUndefinedError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.Identifier) {} + + get location() { + return this.node.loc! + } + + public explain() { + return 'Missing value in variable declaration' + } + + public elaborate() { + return stripIndent` + A variable declaration assigns a value to a name. + For instance, to assign 20 to ${this.node.name}, you can write: + + var ${this.node.name} = 20; + + ${this.node.name} + ${this.node.name}; // 40 + ` + } +} + +const noImplicitDeclareUndefined: Rule = { + name: 'no-implicit-declare-undefined', + + checkers: { + VariableDeclaration(node: es.VariableDeclaration) { + const errors: SourceError[] = [] + for (const decl of node.declarations) { + if (!decl.init) { + errors.push(new NoImplicitDeclareUndefinedError(decl.id as es.Identifier)) + } + } + return errors + } + } +} + +export default noImplicitDeclareUndefined diff --git a/src/slang/rules/noImplicitReturnUndefined.ts b/src/slang/rules/noImplicitReturnUndefined.ts new file mode 100644 index 0000000000..0f3f6741a7 --- /dev/null +++ b/src/slang/rules/noImplicitReturnUndefined.ts @@ -0,0 +1,44 @@ +import { stripIndent } from 'common-tags' +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class NoImplicitReturnUndefinedError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.ReturnStatement) {} + + get location() { + return this.node.loc! + } + + public explain() { + return 'Missing value in return statement' + } + + public elaborate() { + return stripIndent` + This return statement is missing a value. + For instance, to return the value 42, you can write + + return 42; + ` + } +} + +const noImplicitReturnUndefined: Rule = { + name: 'no-implicit-return-undefined', + + checkers: { + ReturnStatement(node: es.ReturnStatement) { + if (!node.argument) { + return [new NoImplicitReturnUndefinedError(node)] + } else { + return [] + } + } + } +} + +export default noImplicitReturnUndefined diff --git a/src/slang/rules/noNonEmptyList.ts b/src/slang/rules/noNonEmptyList.ts new file mode 100644 index 0000000000..d5b3b2512a --- /dev/null +++ b/src/slang/rules/noNonEmptyList.ts @@ -0,0 +1,40 @@ +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class NoNonEmptyListError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.ArrayExpression) {} + + get location() { + return this.node.loc! + } + + public explain() { + return 'Only empty list notation ([]) is allowed' + } + + public elaborate() { + return 'TODO' + } +} + +const noNonEmptyList: Rule = { + name: 'no-non-empty-list', + + disableOn: 9, + + checkers: { + ArrayExpression(node: es.ArrayExpression) { + if (node.elements.length > 0) { + return [new NoNonEmptyListError(node)] + } else { + return [] + } + } + } +} + +export default noNonEmptyList diff --git a/src/slang/rules/singleVariableDeclaration.ts b/src/slang/rules/singleVariableDeclaration.ts new file mode 100644 index 0000000000..bf6c79a48d --- /dev/null +++ b/src/slang/rules/singleVariableDeclaration.ts @@ -0,0 +1,48 @@ +import { generate } from 'astring' +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class MultipleDeclarationsError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + private fixs: es.VariableDeclaration[] + + constructor(public node: es.VariableDeclaration) { + this.fixs = node.declarations.map(declaration => ({ + type: 'VariableDeclaration' as 'VariableDeclaration', + kind: 'var' as 'var', + loc: declaration.loc, + declarations: [declaration] + })) + } + + get location() { + return this.node.loc! + } + + public explain() { + return 'Multiple declaration in a single statement' + } + + public elaborate() { + const fixs = this.fixs.map(n => '\t' + generate(n)).join('\n') + return 'Split the variable declaration into multiple lines as follows\n\n' + fixs + '\n' + } +} + +const singleVariableDeclaration: Rule = { + name: 'single-variable-declaration', + + checkers: { + VariableDeclaration(node: es.VariableDeclaration) { + if (node.declarations.length > 1) { + return [new MultipleDeclarationsError(node)] + } else { + return [] + } + } + } +} + +export default singleVariableDeclaration diff --git a/src/slang/rules/strictEquality.ts b/src/slang/rules/strictEquality.ts new file mode 100644 index 0000000000..b6913cdbe3 --- /dev/null +++ b/src/slang/rules/strictEquality.ts @@ -0,0 +1,42 @@ +import * as es from 'estree' + +import { ErrorSeverity, ErrorType, Rule, SourceError } from '../types' + +export class StrictEqualityError implements SourceError { + public type = ErrorType.SYNTAX + public severity = ErrorSeverity.ERROR + + constructor(public node: es.BinaryExpression) {} + + get location() { + return this.node.loc! + } + + public explain() { + if (this.node.operator === '==') { + return 'Use === instead of ==' + } else { + return 'Use !== instead of !=' + } + } + + public elaborate() { + return '== and != is not a valid operator' + } +} + +const strictEquality: Rule = { + name: 'strict-equality', + + checkers: { + BinaryExpression(node: es.BinaryExpression) { + if (node.operator === '==' || node.operator === '!=') { + return [new StrictEqualityError(node)] + } else { + return [] + } + } + } +} + +export default strictEquality diff --git a/src/slang/schedulers.ts b/src/slang/schedulers.ts new file mode 100644 index 0000000000..012e16a3a0 --- /dev/null +++ b/src/slang/schedulers.ts @@ -0,0 +1,63 @@ +/* tslint:disable: max-classes-per-file */ +import * as es from 'estree' +import { MaximumStackLimitExceeded } from './interpreter-errors' +import { Context, Result, Scheduler, Value } from './types' + +export class AsyncScheduler implements Scheduler { + public run(it: IterableIterator, context: Context): Promise { + return new Promise((resolve, reject) => { + context.runtime.isRunning = true + let itValue = it.next() + try { + while (!itValue.done) { + itValue = it.next() + } + } catch (e) { + resolve({ status: 'error' }) + } finally { + context.runtime.isRunning = false + } + resolve({ + status: 'finished', + value: itValue.value + }) + }) + } +} + +export class PreemptiveScheduler implements Scheduler { + constructor(public steps: number) {} + + public run(it: IterableIterator, context: Context): Promise { + return new Promise((resolve, reject) => { + context.runtime.isRunning = true + let itValue = it.next() + let interval: number + interval = setInterval(() => { + let step = 0 + try { + while (!itValue.done && step < this.steps) { + itValue = it.next() + step++ + } + } catch (e) { + if (/Maximum call stack/.test(e.toString())) { + const stacks: es.CallExpression[] = [] + for (let i = 1; i <= 3; i++) { + stacks.push(context.runtime.frames[i - 1].callExpression!) + } + context.errors.push(new MaximumStackLimitExceeded(context.runtime.nodes[0], stacks)) + } + context.runtime.isRunning = false + clearInterval(interval) + resolve({ status: 'error' }) + } + if (itValue.done) { + context.runtime.isRunning = false + clearInterval(interval) + resolve({ status: 'finished', value: itValue.value }) + } + }) + }) + } +} diff --git a/src/slang/stdlib/list.ts b/src/slang/stdlib/list.ts new file mode 100644 index 0000000000..1e0f109d8c --- /dev/null +++ b/src/slang/stdlib/list.ts @@ -0,0 +1,399 @@ +import { toString } from '../interop' +import { Value } from '../types' + +declare global { + // tslint:disable-next-line:interface-name + interface Function { + __SOURCE__?: string + } +} + +// tslint:disable +// list.js: Supporting lists in the Scheme style, using pairs made +// up of two-element JavaScript array (vector) +// Author: Martin Henz +// Translated to TypeScript by Evan Sebastian + +type List = Value[] + +// array test works differently for Rhino and +// the Firefox environment (especially Web Console) +function array_test(x: Value) { + if (Array.isArray === undefined) { + return x instanceof Array + } else { + return Array.isArray(x) + } +} +array_test.__SOURCE__ = 'array_test(x)' + +// pair constructs a pair using a two-element array +// LOW-LEVEL FUNCTION, NOT SOURCE +export function pair(x: Value, xs: Value) { + return [x, xs] +} +pair.__SOURCE__ = 'pair(x, xs)' + +// is_pair returns true iff arg is a two-element array +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_pair(x: Value) { + return array_test(x) && x.length === 2 +} +is_pair.__SOURCE__ = 'is_pair(x)' + +// head returns the first component of the given pair, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function head(xs: List) { + if (is_pair(xs)) { + return xs[0] + } else { + throw new Error('head(xs) expects a pair as ' + 'argument xs, but encountered ' + toString(xs)) + } +} +head.__SOURCE__ = 'head(xs)' + +// tail returns the second component of the given pair +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE +export function tail(xs: List) { + if (is_pair(xs)) { + return xs[1] + } else { + throw new Error('tail(xs) expects a pair as ' + 'argument xs, but encountered ' + toString(xs)) + } +} +tail.__SOURCE__ = 'tail(xs)' + +// is_empty_list returns true if arg is [] +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_empty_list(xs: List) { + if (array_test(xs)) { + if (xs.length === 0) { + return true + } else if (xs.length === 2) { + return false + } else { + return false + } + } else { + return false + } +} +is_empty_list.__SOURCE__ = 'is_empty_list(xs)' + +// is_list recurses down the list and checks that it ends with the empty list [] +// does not throw Value exceptions +// LOW-LEVEL FUNCTION, NOT SOURCE +export function is_list(xs: List) { + for (; ; xs = tail(xs)) { + if (is_empty_list(xs)) { + return true + } else if (!is_pair(xs)) { + return false + } + } +} +is_list.__SOURCE__ = 'is_list(xs)' + +// list makes a list out of its arguments +// LOW-LEVEL FUNCTION, NOT SOURCE +export function list() { + var the_list = [] + for (var i = arguments.length - 1; i >= 0; i--) { + the_list = pair(arguments[i], the_list) + } + return the_list +} +list.__SOURCE__ = 'list(x, y, ...)' + +// list_to_vector returns vector that contains the elements of the argument list +// in the given order. +// list_to_vector throws an exception if the argument is not a list +// LOW-LEVEL FUNCTION, NOT SOURCE +export function list_to_vector(lst: List) { + var vector = [] + while (!is_empty_list(lst)) { + vector.push(head(lst)) + lst = tail(lst) + } + return vector +} +list_to_vector.__SOURCE__ = 'list_to_vector(xs)' + +// vector_to_list returns a list that contains the elements of the argument vector +// in the given order. +// vector_to_list throws an exception if the argument is not a vector +// LOW-LEVEL FUNCTION, NOT SOURCE +export function vector_to_list(vector: Value[]) { + if (vector.length === 0) { + return [] + } + + var result = [] + for (var i = vector.length - 1; i >= 0; i = i - 1) { + result = pair(vector[i], result) + } + return result +} +vector_to_list.__SOURCE__ = 'vector_to_list(vs)' + +// returns the length of a given argument list +// throws an exception if the argument is not a list +export function length(xs: List) { + for (var i = 0; !is_empty_list(xs); ++i) { + xs = tail(xs) + } + return i +} +length.__SOURCE__ = 'length(xs)' + +// map applies first arg f to the elements of the second argument, +// assumed to be a list. +// f is applied element-by-element: +// map(f,[1,[2,[]]]) results in [f(1),[f(2),[]]] +// map throws an exception if the second argument is not a list, +// and if the second argument is a non-empty list and the first +// argument is not a function. +export function map(f: Function, xs: List): List { + return is_empty_list(xs) ? [] : pair(f(head(xs)), map(f, tail(xs))) +} +map.__SOURCE__ = 'map(f, xs)' + +// build_list takes a non-negative integer n as first argument, +// and a function fun as second argument. +// build_list returns a list of n elements, that results from +// applying fun to the numbers from 0 to n-1. +export function build_list(n: number, fun: Function) { + function build(i: number, fun: Function, already_built: List): List { + if (i < 0) { + return already_built + } else { + return build(i - 1, fun, pair(fun(i), already_built)) + } + } + return build(n - 1, fun, []) +} +build_list.__SOURCE__ = 'build_list(n, fun)' + +// for_each applies first arg fun to the elements of the list passed as +// second argument. fun is applied element-by-element: +// for_each(fun,[1,[2,[]]]) results in the calls fun(1) and fun(2). +// for_each returns true. +// for_each throws an exception if the second argument is not a list, +// and if the second argument is a non-empty list and the +// first argument is not a function. +export function for_each(fun: Function, xs: List) { + if (!is_list(xs)) { + throw new Error('for_each expects a list as argument xs, but ' + 'encountered ' + xs) + } + for (; !is_empty_list(xs); xs = tail(xs)) { + fun(head(xs)) + } + return true +} +for_each.__SOURCE__ = 'for_each(fun, xs)' + +// list_to_string returns a string that represents the argument list. +// It applies itself recursively on the elements of the given list. +// When it encounters a non-list, it applies toString to it. +export function list_to_string(l: List): string { + return toString(l) +} +list_to_string.__SOURCE__ = 'list_to_string(xs)' + +// reverse reverses the argument list +// reverse throws an exception if the argument is not a list. +export function reverse(xs: List) { + if (!is_list(xs)) { + throw new Error('reverse(xs) expects a list as argument xs, but ' + 'encountered ' + xs) + } + var result = [] + for (; !is_empty_list(xs); xs = tail(xs)) { + result = pair(head(xs), result) + } + return result +} +reverse.__SOURCE__ = 'reverse(xs)' + +// append first argument list and second argument list. +// In the result, the [] at the end of the first argument list +// is replaced by the second argument list +// append throws an exception if the first argument is not a list +export function append(xs: List, ys: List): List { + if (is_empty_list(xs)) { + return ys + } else { + return pair(head(xs), append(tail(xs), ys)) + } +} +append.__SOURCE__ = 'append(xs, ys)' + +// member looks for a given first-argument element in a given +// second argument list. It returns the first postfix sublist +// that starts with the given element. It returns [] if the +// element does not occur in the list +export function member(v: Value, xs: List) { + for (; !is_empty_list(xs); xs = tail(xs)) { + if (head(xs) === v) { + return xs + } + } + return [] +} +member.__SOURCE__ = 'member(x, xs)' + +// removes the first occurrence of a given first-argument element +// in a given second-argument list. Returns the original list +// if there is no occurrence. +export function remove(v: Value, xs: List): List { + if (is_empty_list(xs)) { + return [] + } else { + if (v === head(xs)) { + return tail(xs) + } else { + return pair(head(xs), remove(v, tail(xs))) + } + } +} +remove.__SOURCE__ = 'remove(x, xs)' + +// Similar to remove. But removes all instances of v instead of just the first +export function remove_all(v: Value, xs: List): List { + if (is_empty_list(xs)) { + return [] + } else { + if (v === head(xs)) { + return remove_all(v, tail(xs)) + } else { + return pair(head(xs), remove_all(v, tail(xs))) + } + } +} +remove_all.__SOURCE__ = 'remove_all(x, xs)' +// for backwards-compatibility +export const removeAll = remove_all + +// equal computes the structural equality +// over its arguments +export function equal(item1: Value, item2: Value): boolean { + if (is_pair(item1) && is_pair(item2)) { + return equal(head(item1), head(item2)) && equal(tail(item1), tail(item2)) + } else if (array_test(item1) && item1.length === 0 && array_test(item2) && item2.length === 0) { + return true + } else { + return item1 === item2 + } +} +equal.__SOURCE__ = 'equal(x, y)' + +// assoc treats the second argument as an association, +// a list of (index,value) pairs. +// assoc returns the first (index,value) pair whose +// index equal (using structural equality) to the given +// first argument v. Returns false if there is no such +// pair +export function assoc(v: Value, xs: List): boolean { + if (is_empty_list(xs)) { + return false + } else if (equal(v, head(head(xs)))) { + return head(xs) + } else { + return assoc(v, tail(xs)) + } +} +assoc.__SOURCE__ = 'assoc(v, xs)' + +// filter returns the sublist of elements of given list xs +// for which the given predicate function returns true. +export function filter(pred: Function, xs: List): List { + if (is_empty_list(xs)) { + return xs + } else { + if (pred(head(xs))) { + return pair(head(xs), filter(pred, tail(xs))) + } else { + return filter(pred, tail(xs)) + } + } +} +filter.__SOURCE__ = 'filter(pred, xs)' + +// enumerates numbers starting from start, +// using a step size of 1, until the number +// exceeds end. +export function enum_list(start: number, end: number): List { + if (start > end) { + return [] + } else { + return pair(start, enum_list(start + 1, end)) + } +} +enum_list.__SOURCE__ = 'enum_list(start, end)' + +// Returns the item in list lst at index n (the first item is at position 0) +export function list_ref(xs: List, n: number) { + if (n < 0) { + throw new Error( + 'list_ref(xs, n) expects a positive integer as ' + 'argument n, but encountered ' + n + ) + } + + for (; n > 0; --n) { + xs = tail(xs) + } + return head(xs) +} +list_ref.__SOURCE__ = 'list_ref(xs, n)' + +// accumulate applies given operation op to elements of a list +// in a right-to-left order, first apply op to the last element +// and an initial element, resulting in r1, then to the +// second-last element and r1, resulting in r2, etc, and finally +// to the first element and r_n-1, where n is the length of the +// list. +// accumulate(op,zero,list(1,2,3)) results in +// op(1, op(2, op(3, zero))) + +export function accumulate(op: (value: Value, acc: T) => T, initial: T, sequence: List): T { + if (is_empty_list(sequence)) { + return initial + } else { + return op(head(sequence), accumulate(op, initial, tail(sequence))) + } +} +accumulate.__SOURCE__ = 'accumulate(op, initial, xs)' + +// set_head(xs,x) changes the head of given pair xs to be x, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE + +export function set_head(xs: List, x: Value) { + if (is_pair(xs)) { + xs[0] = x + return undefined + } else { + throw new Error( + 'set_head(xs,x) expects a pair as ' + 'argument xs, but encountered ' + toString(xs) + ) + } +} +set_head.__SOURCE__ = 'set_head(xs, x)' + +// set_tail(xs,x) changes the tail of given pair xs to be x, +// throws an exception if the argument is not a pair +// LOW-LEVEL FUNCTION, NOT SOURCE + +export function set_tail(xs: List, x: Value) { + if (is_pair(xs)) { + xs[1] = x + return undefined + } else { + throw new Error( + 'set_tail(xs,x) expects a pair as ' + 'argument xs, but encountered ' + toString(xs) + ) + } +} +set_tail.__SOURCE__ = 'set_tail(xs, x)' +// tslint:enable diff --git a/src/slang/stdlib/misc.ts b/src/slang/stdlib/misc.ts new file mode 100644 index 0000000000..29b254d089 --- /dev/null +++ b/src/slang/stdlib/misc.ts @@ -0,0 +1,61 @@ +/* tslint:disable: ban-types*/ +import { toString } from '../interop' +import { Value } from '../types' + +import { handleConsoleLog } from '../../actions' + +export function display(value: Value) { + const output = toString(value) + // TODO in 2019: fix this hack + if (typeof (window as any).__REDUX_STORE__ !== 'undefined') { + ;(window as any).__REDUX_STORE__.dispatch(handleConsoleLog(output)) + } +} +display.__SOURCE__ = 'display(a)' + +export function error_message(value: Value) { + const output = toString(value) + throw new Error(output) +} +error_message.__SOURCE__ = 'error(a)' + +// tslint:disable-next-line:no-any +export function timed(this: any, f: Function) { + const self = this + const timerType = Date + + return () => { + const start = timerType.now() + const result = f.apply(self, arguments) + const diff = timerType.now() - start + display('Duration: ' + Math.round(diff) + 'ms') + return result + } +} +timed.__SOURCE__ = 'timed(f)' + +export function is_number(v: Value) { + return typeof v === 'number' +} +is_number.__SOURCE__ = 'is_number(v)' + +export function array_length(xs: Value[]) { + return xs.length +} +array_length.__SOURCE__ = 'array_length(xs)' + +export function parse_int(inputString: string, radix: number) { + const parsed = parseInt(inputString, radix) + if (inputString && radix && parsed) { + // the two arguments are provided, and parsed is not NaN + return parsed + } else { + throw new Error('parseInt expects two arguments a string s, and a positive integer i') + } +} +parse_int.__SOURCE__ = 'parse_int(s, i)' + +export function runtime() { + return new Date().getTime() +} +runtime.__SOURCE__ = 'runtime()' diff --git a/src/slang/stdlib/object.ts b/src/slang/stdlib/object.ts new file mode 100644 index 0000000000..5d7e09900b --- /dev/null +++ b/src/slang/stdlib/object.ts @@ -0,0 +1,5 @@ +import { Value } from '../types' + +export function is_instance_of(a: Value, b: Value) { + return a instanceof b +} diff --git a/src/slang/syntaxTypes.ts b/src/slang/syntaxTypes.ts new file mode 100644 index 0000000000..0efe756b42 --- /dev/null +++ b/src/slang/syntaxTypes.ts @@ -0,0 +1,53 @@ +const syntaxTypes: { [nodeName: string]: number } = { + // Week 3 + Program: 3, + ExpressionStatement: 3, + IfStatement: 3, + FunctionDeclaration: 3, + VariableDeclaration: 3, + ReturnStatement: 3, + CallExpression: 3, + UnaryExpression: 3, + BinaryExpression: 3, + LogicalExpression: 3, + ConditionalExpression: 3, + FunctionExpression: 3, + ArrowFunctionExpression: 3, + Identifier: 3, + Literal: 3, + + // Week 5 + EmptyStatement: 5, + ArrayExpression: 5, + + // Week 8 + AssignmentExpression: 8, + WhileStatement: 8, + + // Week 9 + ForStatement: 9, + BreakStatement: 9, + ContinueStatement: 9, + MemberExpression: 9, + + // Week 10 + ThisExpression: 10, + ObjectExpression: 10, + Property: 10, + UpdateExpression: 10, + NewExpression: 10, + + // Disallowed Forever + SwitchStatement: Infinity, + DebuggerStatement: Infinity, + WithStatement: Infinity, + LabeledStatement: Infinity, + SwitchCase: Infinity, + ThrowStatement: Infinity, + CatchClause: Infinity, + DoWhileStatement: Infinity, + ForInStatement: Infinity, + SequenceExpression: Infinity +} + +export default syntaxTypes diff --git a/src/slang/types.ts b/src/slang/types.ts new file mode 100644 index 0000000000..6a9c186597 --- /dev/null +++ b/src/slang/types.ts @@ -0,0 +1,198 @@ +/* tslint:disable:interface-name max-classes-per-file */ + +import { SourceLocation } from 'acorn' +import * as es from 'estree' + +import { closureToJS } from './interop' + +export enum ErrorType { + SYNTAX = 'Syntax', + TYPE = 'Type', + RUNTIME = 'Runtime' +} + +export enum ErrorSeverity { + WARNING = 'Warning', + ERROR = 'Error' +} + +export interface SourceError { + type: ErrorType + severity: ErrorSeverity + location: es.SourceLocation + explain(): string + elaborate(): string +} + +export interface Rule { + name: string + disableOn?: number + checkers: { + [name: string]: (node: T) => SourceError[] + } +} + +// tslint:disable-next-line:no-namespace +export namespace CFG { + // tslint:disable-next-line:no-shadowed-variable + export interface Scope { + name: string + parent?: Scope + entry?: Vertex + exits: Vertex[] + node?: es.Node + proof?: es.Node + type: Type + env: { + [name: string]: Sym + } + } + + export interface Vertex { + id: string + node: es.Node + scope?: Scope + usages: Sym[] + } + + export interface Sym { + name: string + defined?: boolean + definedAt?: es.SourceLocation + type: Type + proof?: es.Node + } + + export interface Type { + name: 'number' | 'string' | 'boolean' | 'function' | 'undefined' | 'any' + params?: Type[] + returnType?: Type + } + + export type EdgeLabel = 'next' | 'alternate' | 'consequent' + + export interface Edge { + type: EdgeLabel + to: Vertex + } +} + +export interface Comment { + type: 'Line' | 'Block' + value: string + start: number + end: number + loc: SourceLocation | undefined +} + +export interface TypeError extends SourceError { + expected: CFG.Type[] + got: CFG.Type + proof?: es.Node +} + +export interface Context { + /** The source version used */ + week: number + + /** All the errors gathered */ + errors: SourceError[] + + /** CFG Specific State */ + cfg: { + nodes: { [id: string]: CFG.Vertex } + edges: { [from: string]: CFG.Edge[] } + scopes: CFG.Scope[] + } + + /** Runtime Sepecific state */ + runtime: { + isRunning: boolean + frames: Frame[] + nodes: es.Node[] + } +} + +// tslint:disable:no-any +export interface Environment { + [name: string]: any +} +export type Value = any +// tslint:enable:no-any + +export interface Frame { + name: string + parent: Frame | null + callExpression?: es.CallExpression + environment: Environment + thisContext?: Value +} + +/** + * Models function value in the interpreter environment. + */ +export class Closure { + /** Keep track how many lambdas are created */ + private static lambdaCtr = 0 + + /** Unique ID defined for anonymous closure */ + public name: string + + /** Fake closure function */ + // tslint:disable-next-line:ban-types + public fun: Function + + constructor(public node: es.FunctionExpression, public frame: Frame, context: Context) { + this.node = node + try { + if (this.node.id) { + this.name = this.node.id.name + } + } catch (e) { + this.name = `Anonymous${++Closure.lambdaCtr}` + } + this.fun = closureToJS(this, context, this.name) + } +} + +/** + * Modified from class Closure, for construction of arrow functions. + */ +export class ArrowClosure { + /** Keep track how many lambdas are created */ + private static arrowCtr = 0 + + /** Unique ID defined for anonymous closure */ + public name: string + + /** Fake closure function */ + // tslint:disable-next-line:ban-types + public fun: Function + + constructor(public node: es.Function, public frame: Frame, context: Context) { + this.name = `Anonymous${++ArrowClosure.arrowCtr}` + this.fun = closureToJS(this, context, this.name) + } +} + +interface Error { + status: 'error' +} + +export interface Finished { + status: 'finished' + value: Value +} + +interface Suspended { + status: 'suspended' + it: IterableIterator + scheduler: Scheduler + context: Context +} + +export type Result = Suspended | Finished | Error + +export interface Scheduler { + run(it: IterableIterator, context: Context): Promise +} diff --git a/src/slang/typings/acorn.d.ts b/src/slang/typings/acorn.d.ts new file mode 100644 index 0000000000..3e3e948ff6 --- /dev/null +++ b/src/slang/typings/acorn.d.ts @@ -0,0 +1,49 @@ +declare module 'acorn/dist/walk' { + import * as es from 'estree' + + namespace AcornWalk { + export type SimpleWalker = (node: es.Node, state?: S) => void + export type SimpleWalkers = { [name: string]: SimpleWalker } + export type Walker = ( + node: T, + state: S, + callback: SimpleWalker + ) => void + export type Walkers = { [name: string]: Walker } + type NodeTest = (nodeType: string, node: es.Node) => boolean + + export const base: Walkers + + export function simple( + node: es.Node, + visitors: SimpleWalkers, + base?: SimpleWalkers, + state?: S + ): void + export function recursive(node: es.Node, state: S, functions: Walkers): void + export function findNodeAt( + node: es.Node, + start: null | number, + end: null | number, + test: string | NodeTest, + base?: SimpleWalkers, + state?: S + ): void + export function findNodeAround( + node: es.Node, + pos: es.Position, + test: string | NodeTest, + base?: SimpleWalkers, + state?: S + ): void + export function findNodeAfter( + node: es.Node, + pos: es.Position, + test: string | NodeTest, + base?: SimpleWalkers, + state?: S + ): void + } + + export = AcornWalk +} diff --git a/src/slang/typings/astring.d.ts b/src/slang/typings/astring.d.ts new file mode 100644 index 0000000000..7f3a6fd2f2 --- /dev/null +++ b/src/slang/typings/astring.d.ts @@ -0,0 +1 @@ +declare module 'astring' diff --git a/src/slang/typings/estree.d.ts b/src/slang/typings/estree.d.ts new file mode 100644 index 0000000000..55ca251ea8 --- /dev/null +++ b/src/slang/typings/estree.d.ts @@ -0,0 +1,8 @@ +import * as estree from 'estree' + +declare module 'estree' { + interface BaseNode { + __id?: string + __call?: string + } +} diff --git a/src/slang/utils/node.ts b/src/slang/utils/node.ts new file mode 100644 index 0000000000..3f2c9cc874 --- /dev/null +++ b/src/slang/utils/node.ts @@ -0,0 +1,118 @@ +/** + * Utility functions to work with the AST (Abstract Syntax Tree) + */ +import { SimpleWalker, Walker } from 'acorn/dist/walk' +import * as es from 'estree' +import { Closure, Value } from '../types' + +/** + * Check whether two nodes are equal. + * + * Two nodes are equal if their `__id` field are equal, or + * if they have `__call`, the `__call` field is checked instead. + * + * @param n1 First node + * @param n2 Second node + */ +export const isNodeEqual = (n1: es.Node, n2: es.Node) => { + if (n1.hasOwnProperty('__id') && n2.hasOwnProperty('__id')) { + const first = n1.__id === n2.__id + if (!first) { + return false + } + if (n1.hasOwnProperty('__call') && n2.hasOwnProperty('__call')) { + return n1.__call === n2.__call + } else { + return true + } + } else { + return n1 === n2 + } +} + +/** + * Non-destructively search for a node in a parent node and replace it with another node. + * + * @param node The root node to be searched + * @param before Node to be replaced + * @param after Replacement node + */ +export const replaceAST = (node: es.Node, before: es.Node, after: es.Node) => { + let found = false + + const go = (n: es.Node): {} => { + if (found) { + return n + } + + if (isNodeEqual(n, before)) { + found = true + return after + } + + if (n.type === 'CallExpression') { + return { ...n, callee: go(n.callee), arguments: n.arguments.map(go) } + } else if (n.type === 'ConditionalExpression') { + return { + ...n, + test: go(n.test), + consequent: go(n.consequent), + alternate: go(n.alternate) + } + } else if (n.type === 'UnaryExpression') { + return { ...n, argument: go(n.argument) } + } else if (n.type === 'BinaryExpression' || n.type === 'LogicalExpression') { + return { ...n, left: go(n.left), right: go(n.right) } + } else { + return n + } + } + + return go(node) +} + +const createLiteralNode = (value: {}): es.Node => { + if (typeof value === 'undefined') { + return { + type: 'Identifier', + name: 'undefined', + __id: freshId() + } + } else { + return { + type: 'Literal', + value, + raw: value, + __id: freshId() + } as es.SimpleLiteral + } +} + +const freshId = (() => { + let id = 0 + + return () => { + id++ + return '__syn' + id + } +})() + +/** + * Create an AST node from a Source value. + * + * @param value any valid Source value (number/string/boolean/Closure) + * @returns {Node} + */ +export const createNode = (value: Value): es.Node => { + if (value instanceof Closure) { + return value.node + } + return createLiteralNode(value) +} + +export const composeWalker = (w1: Walker, w2: Walker) => { + return (node: T, state: S, recurse: SimpleWalker) => { + w1(node, state, recurse) + w2(node, state, recurse) + } +} diff --git a/src/slang/utils/rttc.ts b/src/slang/utils/rttc.ts new file mode 100644 index 0000000000..7a87e6c80a --- /dev/null +++ b/src/slang/utils/rttc.ts @@ -0,0 +1,89 @@ +import * as es from 'estree' +import { Context, ErrorSeverity, ErrorType, SourceError, Value } from '../types' + +class TypeError implements SourceError { + public type: ErrorType.RUNTIME + public severity: ErrorSeverity.WARNING + public location: es.SourceLocation + + constructor(node: es.Node, public context: string, public expected: string, public got: string) { + this.location = node.loc! + } + + public explain() { + return `TypeError: Expected ${this.expected} in ${this.context}, got ${this.got}.` + } + + public elaborate() { + return 'TODO' + } +} + +const isNumber = (v: Value) => typeof v === 'number' +const isString = (v: Value) => typeof v === 'string' + +const checkAdditionAndComparison = (context: Context, left: Value, right: Value) => { + if (!(isNumber(left) || isString(left))) { + context.errors.push( + new TypeError( + context.runtime.nodes[0], + 'left hand side of operation', + 'number or string', + typeof left + ) + ) + } + if (!(isNumber(right) || isString(right))) { + context.errors.push( + new TypeError( + context.runtime.nodes[0], + 'right hand side of operation', + 'number or string', + typeof right + ) + ) + } +} + +const checkBinaryArithmetic = (context: Context, left: Value, right: Value) => { + if (!isNumber(left)) { + context.errors.push( + new TypeError(context.runtime.nodes[0], 'left hand side of operation', 'number', typeof left) + ) + } + if (!isNumber(left)) { + context.errors.push( + new TypeError( + context.runtime.nodes[0], + 'right hand side of operation', + 'number', + typeof right + ) + ) + } +} + +export const checkBinaryExpression = ( + context: Context, + operator: es.BinaryOperator, + left: Value, + right: Value +) => { + switch (operator) { + case '-': + case '*': + case '/': + case '%': + return checkBinaryArithmetic(context, left, right) + case '<': + case '<=': + case '>': + case '>=': + case '+': + return checkAdditionAndComparison(context, left, right) + case '!==': + case '===': + default: + return + } +} diff --git a/src/styles/ide.css b/src/styles/ide.css new file mode 100644 index 0000000000..d6427dda52 --- /dev/null +++ b/src/styles/ide.css @@ -0,0 +1,19 @@ +.IDE { + display: flex; + flex-direction: column; +} + +.IDE_main { + margin-top: 1rem; + margin-left: 1rem; +} + +.Repl > .pt-card { + font-family: Monospace; + white-space: pre-line; +} + +.IDE > .row { + margin-right: 0px; + margin-left: 0px; +} diff --git a/src/styles/index.css b/src/styles/index.css index 8cff40f342..90b5ba2286 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,6 +1,7 @@ @import "~normalize.css/normalize.css"; @import "~@blueprintjs/core/lib/css/blueprint.css"; @import "./blueprint.css"; +@import "~flexboxgrid/dist/flexboxgrid.css"; @import "./application.css"; -@import "./playground.css"; +@import "./ide.css"; @import "./navigationBar.css"; diff --git a/tsconfig.json b/tsconfig.json index 342f99e29e..aab220573a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,8 +2,8 @@ "compilerOptions": { "outDir": "build/dist", "module": "esnext", - "target": "es5", - "lib": ["es6", "dom", "es2015"], + "target": "es2016", + "lib": ["es6", "dom", "es2015", "es2017"], "sourceMap": true, "allowJs": true, "jsx": "react", diff --git a/tslint.json b/tslint.json index 38976e529b..dcfa9ebc13 100644 --- a/tslint.json +++ b/tslint.json @@ -1,6 +1,8 @@ { "extends": ["tslint:recommended", "tslint-react", "tslint-config-prettier"], "rules": { + "interface-over-type-literal": false, + "no-empty": false, "object-literal-sort-keys": false }, "linterOptions": { diff --git a/yarn.lock b/yarn.lock index ce7d882bca..7a58c552c5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -36,6 +36,12 @@ classnames "^2.2" tslib "^1.9.0" +"@types/acorn@^4.0.3": + version "4.0.3" + resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.3.tgz#d1f3e738dde52536f9aad3d3380d14e448820afd" + dependencies: + "@types/estree" "*" + "@types/cheerio@*": version "0.22.7" resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.7.tgz#4a92eafedfb2b9f4437d3a4410006d81114c66ce" @@ -44,6 +50,10 @@ version "2.2.3" resolved "https://registry.yarnpkg.com/@types/classnames/-/classnames-2.2.3.tgz#3f0ff6873da793870e20a260cada55982f38a9e5" +"@types/common-tags@^1.4.0": + version "1.4.0" + resolved "https://registry.yarnpkg.com/@types/common-tags/-/common-tags-1.4.0.tgz#28c1be61e352dde38936018984e2885caef087c1" + "@types/dom4@^2.0.0": version "2.0.0" resolved "https://registry.yarnpkg.com/@types/dom4/-/dom4-2.0.0.tgz#00dc42fed6b36a7a6dabb8f7a9c9e678ee644e05" @@ -68,10 +78,18 @@ "@types/cheerio" "*" "@types/react" "*" +"@types/estree@*", "@types/estree@^0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + "@types/history@*": version "4.6.2" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.6.2.tgz#12cfaba693ba20f114ed5765467ff25fdf67ddb0" +"@types/invariant@^2.2.29": + version "2.2.29" + resolved "https://registry.yarnpkg.com/@types/invariant/-/invariant-2.2.29.tgz#aa845204cd0a289f65d47e0de63a6a815e30cc66" + "@types/jest@^22.2.3": version "22.2.3" resolved "https://registry.yarnpkg.com/@types/jest/-/jest-22.2.3.tgz#0157c0316dc3722c43a7b71de3fdf3acbccef10d" @@ -406,6 +424,10 @@ astral-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-1.0.0.tgz#6c8c3fb827dd43ee3918f27b82782ab7658a6fd9" +astring@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/astring/-/astring-1.3.0.tgz#7ed6ff7d317df5d4a7a06a42b5097774d8d48e01" + async-each@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" @@ -1686,6 +1708,12 @@ commander@~2.13.0: version "2.13.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.13.0.tgz#6964bca67685df7c1f1430c584f07d7597885b9c" +common-tags@^1.7.2: + version "1.7.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.7.2.tgz#24d9768c63d253a56ecff93845b44b4df1d52771" + dependencies: + babel-runtime "^6.26.0" + commondir@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" @@ -2925,6 +2953,10 @@ flatten@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/flatten/-/flatten-1.0.2.tgz#dae46a9d78fbe25292258cc1e780a41d95c03782" +flexboxgrid@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/flexboxgrid/-/flexboxgrid-6.3.1.tgz#e99898afc07b7047722bb81a958a5fba4d4e20fd" + flush-write-stream@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.3.tgz#c5d586ef38af6097650b49bc41b55fabb19f35bd" @@ -6352,6 +6384,10 @@ redux-mock-store@^1.5.1: dependencies: lodash.isplainobject "^4.0.6" +redux-saga@^0.15.6: + version "0.15.6" + resolved "https://registry.yarnpkg.com/redux-saga/-/redux-saga-0.15.6.tgz#8638dc522de6c6c0a496fe8b2b5466287ac2dc4d" + redux@^3.6.0, redux@^3.7.2: version "3.7.2" resolved "https://registry.yarnpkg.com/redux/-/redux-3.7.2.tgz#06b73123215901d25d065be342eb026bc1c8537b"