diff --git a/abra_wasm/abra_wasm.d.ts b/abra_wasm/abra_wasm.d.ts index 864bfd1..456f85d 100644 --- a/abra_wasm/abra_wasm.d.ts +++ b/abra_wasm/abra_wasm.d.ts @@ -3,6 +3,25 @@ import { Error } from './types/error' import { Module } from './types/module' +export interface TypecheckSuccess { + success: true, +} + +export interface TypecheckFailure { + success: false, + error: Error, + errorMessage: string +} + +export type TypecheckResult = TypecheckSuccess | TypecheckFailure + +/** + * Reads the input string as Abra code, and typechecks it, using the wasm implementation + * of the compiler. + * This will either return an error if unsuccessful, or nothing if successful. + */ +export function typecheck(input: string): TypecheckResult | null; + export interface CompileSuccess { success: true, module: Module @@ -10,7 +29,8 @@ export interface CompileSuccess { export interface CompileFailure { success: false, - error: Error + error: Error, + errorMessage: string } export type CompileResult = CompileSuccess | CompileFailure @@ -21,17 +41,30 @@ export type CompileResult = CompileSuccess | CompileFailure */ export function compile(input: string): CompileResult | null; +export interface RunSuccess { + success: true, + data: any +} + +export interface RunFailure { + success: false, + error: Error, + errorMessage: string +} + +export type RunResult = RunSuccess | RunFailure + /** * Compiles and executes the input string as Abra code, returning the result. This could * result in a runtime error. */ -export function runSync(input: string): Error | any; +export function runSync(input: string): RunResult; /** * Compiles and executes the input string as Abra code, resolving with the * result. This could result in a runtime error, which will also resolve as a successful Promise */ -export function runAsync(input: string): Promise; +export function runAsync(input: string): Promise; export interface DisassembleSuccess { success: true, @@ -40,7 +73,8 @@ export interface DisassembleSuccess { export interface DisassembleFailure { success: false, - error: Error + error: Error, + errorMessage: string } export type DisassembleResult = DisassembleSuccess | DisassembleFailure diff --git a/abra_wasm/abra_wasm.js b/abra_wasm/abra_wasm.js index 8e6d2dd..b52a1b8 100644 --- a/abra_wasm/abra_wasm.js +++ b/abra_wasm/abra_wasm.js @@ -65,7 +65,7 @@ function makeMutClosure(arg0, arg1, dtor, f) { real.original = state; return real; } -function __wbg_adapter_12(arg0, arg1, arg2) { +function __wbg_adapter_10(arg0, arg1, arg2) { wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__hd79dee99152d20bc(arg0, arg1, addHeapObject(arg2)); } @@ -134,6 +134,17 @@ export function disassemble(input) { return takeObject(ret); } +/** +* @param {string} input +* @returns {any} +*/ +export function typecheck(input) { + var ptr0 = passStringToWasm0(input, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc); + var len0 = WASM_VECTOR_LEN; + var ret = wasm.typecheck(ptr0, len0); + return takeObject(ret); +} + /** * @param {string} input * @returns {any} @@ -170,7 +181,7 @@ export function runAsync(input) { function handleError(e) { wasm.__wbindgen_exn_store(addHeapObject(e)); } -function __wbg_adapter_21(arg0, arg1, arg2, arg3) { +function __wbg_adapter_20(arg0, arg1, arg2, arg3) { wasm.wasm_bindgen__convert__closures__invoke2_mut__h7f0356f533042ecb(arg0, arg1, addHeapObject(arg2), addHeapObject(arg3)); } @@ -220,10 +231,6 @@ async function init(input) { imports.wbg.__wbindgen_object_drop_ref = function(arg0) { takeObject(arg0); }; - imports.wbg.__wbindgen_string_new = function(arg0, arg1) { - var ret = getStringFromWasm0(arg0, arg1); - return addHeapObject(ret); - }; imports.wbg.__wbg_log_022eb750364e566f = function(arg0, arg1) { console.log(getStringFromWasm0(arg0, arg1)); }; @@ -251,7 +258,7 @@ async function init(input) { const a = state0.a; state0.a = 0; try { - return __wbg_adapter_21(a, state0.b, arg0, arg1); + return __wbg_adapter_20(a, state0.b, arg0, arg1); } finally { state0.a = a; } @@ -273,8 +280,8 @@ async function init(input) { imports.wbg.__wbindgen_throw = function(arg0, arg1) { throw new Error(getStringFromWasm0(arg0, arg1)); }; - imports.wbg.__wbindgen_closure_wrapper702 = function(arg0, arg1, arg2) { - var ret = makeMutClosure(arg0, arg1, 159, __wbg_adapter_12); + imports.wbg.__wbindgen_closure_wrapper737 = function(arg0, arg1, arg2) { + var ret = makeMutClosure(arg0, arg1, 172, __wbg_adapter_10); return addHeapObject(ret); }; diff --git a/abra_wasm/abra_wasm_bg.d.ts b/abra_wasm/abra_wasm_bg.d.ts index 1653917..4aec6e7 100644 --- a/abra_wasm/abra_wasm_bg.d.ts +++ b/abra_wasm/abra_wasm_bg.d.ts @@ -2,6 +2,7 @@ /* eslint-disable */ export const memory: WebAssembly.Memory; export function disassemble(a: number, b: number): number; +export function typecheck(a: number, b: number): number; export function compile(a: number, b: number): number; export function runSync(a: number, b: number): number; export function runAsync(a: number, b: number): number; diff --git a/abra_wasm/abra_wasm_bg.wasm b/abra_wasm/abra_wasm_bg.wasm index 13b803d..2192ed3 100644 Binary files a/abra_wasm/abra_wasm_bg.wasm and b/abra_wasm/abra_wasm_bg.wasm differ diff --git a/abra_wasm/types/error.d.ts b/abra_wasm/types/error.d.ts index e8a3b8d..078fb07 100644 --- a/abra_wasm/types/error.d.ts +++ b/abra_wasm/types/error.d.ts @@ -1,5 +1,5 @@ import { Token } from './token' -import { Position } from './position' +import { Position, Range } from './position' import { Type } from './abra-types' import { BinaryOp } from './binary-op' @@ -9,37 +9,34 @@ export type Error | Errors.TypecheckerError | Errors.InterpretError +interface BaseError { + range: Range | null +} + export namespace Errors { export type ParseError = ParseErrors.UnexpectedToken | ParseErrors.UnexpectedEof | ParseErrors.ExpectedToken - | ParseErrors.Raw export namespace ParseErrors { - interface UnexpectedToken { + interface UnexpectedToken extends BaseError { kind: 'parseError', subKind: 'unexpectedToken', token: Token } - interface UnexpectedEof { + interface UnexpectedEof extends BaseError { kind: 'parseError', subKind: 'unexpectedEof' } - interface ExpectedToken { + interface ExpectedToken extends BaseError { kind: 'parseError', subKind: 'expectedToken', expectedType: string, token: Token } - - interface Raw { - kind: 'parseError', - subKind: 'raw', - msg: string - } } export type LexerError @@ -48,20 +45,20 @@ export namespace Errors { | LexerErrors.UnterminatedString export namespace LexerErrors { - interface UnexpectedEof { + interface UnexpectedEof extends BaseError { kind: 'lexerError', subKind: 'unexpectedEof', pos: Position } - interface UnexpectedChar { + interface UnexpectedChar extends BaseError { kind: 'lexerError', subKind: 'unexpectedChar', pos: Position, char: string } - interface UnterminatedString { + interface UnterminatedString extends BaseError { kind: 'lexerError', subKind: 'unterminatedString', startPos: Position, @@ -71,9 +68,12 @@ export namespace Errors { export type TypecheckerError = TypecheckerErrors.Mismatch + | TypecheckerErrors.InvalidIfConditionType | TypecheckerErrors.InvalidOperator | TypecheckerErrors.MissingRequiredAssignment | TypecheckerErrors.DuplicateBinding + | TypecheckerErrors.DuplicateField + | TypecheckerErrors.DuplicateType | TypecheckerErrors.UnknownIdentifier | TypecheckerErrors.InvalidAssignmentTarget | TypecheckerErrors.AssignmentToImmutable @@ -83,10 +83,26 @@ export namespace Errors { | TypecheckerErrors.IfExprBranchMismatch | TypecheckerErrors.InvalidInvocationTarget | TypecheckerErrors.IncorrectArity - | TypecheckerErrors.ParamNameMismatch + | TypecheckerErrors.UnexpectedParamName + | TypecheckerErrors.DuplicateParamName + | TypecheckerErrors.RecursiveRefWithoutReturnType + | TypecheckerErrors.InvalidBreak + | TypecheckerErrors.InvalidRequiredArgPosition + | TypecheckerErrors.InvalidIndexingTarget + | TypecheckerErrors.InvalidIndexingSelector + | TypecheckerErrors.UnknownMember + | TypecheckerErrors.MissingRequiredParams + | TypecheckerErrors.InvalidMixedParamType + | TypecheckerErrors.InvalidTypeFuncInvocation + | TypecheckerErrors.InvalidSelfParamPosition + | TypecheckerErrors.InvalidSelfParam + | TypecheckerErrors.MissingRequiredTypeAnnotation + | TypecheckerErrors.InvalidTypeDeclDepth + | TypecheckerErrors.ForbiddenUnknownType + | TypecheckerErrors.InvalidInstantiation export namespace TypecheckerErrors { - interface Mismatch { + interface Mismatch extends BaseError { kind: 'typecheckerError', subKind: 'mismatch', token: Token, @@ -94,7 +110,14 @@ export namespace Errors { actual: Type } - interface InvalidOperator { + interface InvalidIfConditionType extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidIfConditionType', + token: Token, + actual: Type + } + + interface InvalidOperator extends BaseError { kind: 'typecheckerError', subKind: 'invalidOperator', token: Token, @@ -103,71 +126,89 @@ export namespace Errors { rType: Type } - interface MissingRequiredAssignment { + interface MissingRequiredAssignment extends BaseError { kind: 'typecheckerError', subKind: 'missingRequiredAssignment', ident: Token } - interface DuplicateBinding { + interface DuplicateBinding extends BaseError { kind: 'typecheckerError', subKind: 'duplicateBinding', ident: Token, origIdent: Token } - interface UnknownIdentifier { + interface DuplicateField extends BaseError { + kind: 'typecheckerError', + subKind: 'duplicateField', + ident: Token, + origIdent: Token + origType: Type + } + + interface DuplicateType extends BaseError { + kind: 'typecheckerError', + subKind: 'duplicateType', + ident: Token, + origIdent: Token | null + } + + interface UnknownIdentifier extends BaseError { kind: 'typecheckerError', subKind: 'unknownIdentifier', ident: Token } - interface InvalidAssignmentTarget { + interface InvalidAssignmentTarget extends BaseError { kind: 'typecheckerError', subKind: 'invalidAssignmentTarget', token: Token } - interface AssignmentToImmutable { + interface AssignmentToImmutable extends BaseError { kind: 'typecheckerError', subKind: 'assignmentToImmutable', token: Token, origIdent: Token, } - interface UnannotatedUninitialized { + interface UnannotatedUninitialized extends BaseError { kind: 'typecheckerError', subKind: 'unannotatedUninitialized', ident: Token, - isImmutable: boolean + isMutable: boolean } - interface UnknownType { + interface UnknownType extends BaseError { kind: 'typecheckerError', subKind: 'unknownType', typeIdent: Token } - interface MissingIfExprBranch { + interface MissingIfExprBranch extends BaseError { kind: 'typecheckerError', subKind: 'missingIfExprBranch', - typeIdent: Token + ifToken: Token, + isIfBranch: boolean } - interface IfExprBranchMismatch { + interface IfExprBranchMismatch extends BaseError { kind: 'typecheckerError', subKind: 'ifExprBranchMismatch', ifToken: Token, - isIfBranch: boolean + ifType: Type, + elseType: Type, } - interface InvalidInvocationTarget { + interface InvalidInvocationTarget extends BaseError { kind: 'typecheckerError', subKind: 'invalidInvocationTarget', - token: Token + token: Token, + targetType: Type } - interface IncorrectArity { + interface IncorrectArity extends BaseError { kind: 'typecheckerError', subKind: 'incorrectArity', token: Token, @@ -175,12 +216,113 @@ export namespace Errors { actual: number } - interface ParamNameMismatch { + interface UnexpectedParamName extends BaseError { + kind: 'typecheckerError', + subKind: 'unexpectedParamName', + token: Token + } + + interface DuplicateParamName extends BaseError { kind: 'typecheckerError', - subKind: 'paramNameMismatch', + subKind: 'duplicateParamName', + token: Token + } + + interface RecursiveRefWithoutReturnType extends BaseError { + kind: 'typecheckerError', + subKind: 'recursiveRefWithoutReturnType', token: Token, - expected: string, - actual: string + origToken: Token + } + + interface InvalidBreak extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidBreak', + token: Token + } + + interface InvalidRequiredArgPosition extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidRequiredArgPosition', + token: Token + } + + interface InvalidIndexingTarget extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidIndexingTarget', + token: Token, + targetType: Type + } + + interface InvalidIndexingSelector extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidIndexingSelector', + token: Token, + targetType: Type, + selectorType: Type + } + + interface UnknownMember extends BaseError { + kind: 'typecheckerError', + subKind: 'unknownMember', + token: Token, + targetType: Type + } + + interface MissingRequiredParams extends BaseError { + kind: 'typecheckerError', + subKind: 'missingRequiredParams', + token: Token, + missingParams: string[] + } + + interface InvalidMixedParamType extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidMixedParamType', + token: Token + } + + interface InvalidTypeFuncInvocation extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidTypeFuncInvocation', + token: Token + } + + interface InvalidSelfParamPosition extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidSelfParamPosition', + token: Token + } + + interface InvalidSelfParam extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidSelfParam', + token: Token + } + + interface MissingRequiredTypeAnnotation extends BaseError { + kind: 'typecheckerError', + subKind: 'missingRequiredTypeAnnotation', + token: Token + } + + interface InvalidTypeDeclDepth extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidTypeDeclDepth', + token: Token + } + + interface ForbiddenUnknownType extends BaseError { + kind: 'typecheckerError', + subKind: 'forbiddenUnknownType', + token: Token + } + + interface InvalidInstantiation extends BaseError { + kind: 'typecheckerError', + subKind: 'invalidInstantiation', + token: Token, + type: Type } } @@ -191,22 +333,22 @@ export namespace Errors { | InterpretErrors.TypeError export namespace InterpretErrors { - interface StackEmpty { + interface StackEmpty extends BaseError { kind: 'interpretError', subKind: 'stackEmpty' } - interface ConstIdxOutOfBounds { + interface ConstIdxOutOfBounds extends BaseError { kind: 'interpretError', subKind: 'constIdxOutOfBounds' } - interface EndOfBytes { + interface EndOfBytes extends BaseError { kind: 'interpretError', subKind: 'endOfBytes' } - interface TypeError { + interface TypeError extends BaseError { kind: 'interpretError', subKind: 'typeError', expected: string, diff --git a/abra_wasm/types/position.d.ts b/abra_wasm/types/position.d.ts index fc784ac..f601d3e 100644 --- a/abra_wasm/types/position.d.ts +++ b/abra_wasm/types/position.d.ts @@ -1 +1,6 @@ export type Position = [number, number] + +export interface Range { + start: Position, + end: Position +} diff --git a/public/abra_wasm/abra_wasm_bg.wasm b/public/abra_wasm/abra_wasm_bg.wasm index 13b803d..2192ed3 100644 Binary files a/public/abra_wasm/abra_wasm_bg.wasm and b/public/abra_wasm/abra_wasm_bg.wasm differ diff --git a/src/abra-lang/abra-mode.ts b/src/abra-lang/abra-mode.ts index 962abfc..6df1d68 100644 --- a/src/abra-lang/abra-mode.ts +++ b/src/abra-lang/abra-mode.ts @@ -7,12 +7,12 @@ type HandlerFn = (stream: StringStream, state: ModeState) => string | null interface ModeState { baseCol: number, indentDepth: number, - current: HandlerFn + current: HandlerFn, + definedTypes: string[] } CodeMirror.defineMode('abra', () => { - const keywords = ['val', 'var', 'func', 'if', 'else', 'for', 'in', 'while', 'break'] - const builtins = ['Bool', 'Int', 'Float', 'String'] + const keywords = ['val', 'var', 'func', 'if', 'else', 'for', 'in', 'while', 'break', 'type', 'None', 'self'] const indentTokens = ['{', '[', '('] const unindentTokens = ['}', ']', ')'] @@ -78,20 +78,41 @@ CodeMirror.defineMode('abra', () => { } } + function type(stream: StringStream, state: ModeState) { + const typeName = [] + let ch = stream.next() + while (ch && /\S/.test(ch)) { + typeName.push(ch) + ch = stream.next() + } + state.definedTypes.push(typeName.join('')) + state.current = normal + return 'variable-2' + } + const mode: Mode = { startState() { - return { baseCol: 0, indentDepth: 0, current: normal } + return { + baseCol: 0, + indentDepth: 0, + current: normal, + definedTypes: ['Bool', 'Int', 'Float', 'String', 'Unit'] + } }, token(stream, state) { if (stream.eatSpace()) return null const style = state.current(stream, state) const word = stream.current() + if (word === 'type') { + state.current = type + } + if (style === 'comment') return style if (style === 'variable') { if (keywords.includes(word)) return 'keyword' - else if (builtins.includes(word)) return 'variable-2' + else if (state.definedTypes.includes(word)) return 'variable-2' return null } diff --git a/src/abra-lang/wrapper.ts b/src/abra-lang/wrapper.ts new file mode 100644 index 0000000..b0e7249 --- /dev/null +++ b/src/abra-lang/wrapper.ts @@ -0,0 +1,30 @@ +import * as Abra from 'abra_wasm' + +const _init = Abra.default + +let _abraInitPromise: Promise | null = null + +function init() { + if (_abraInitPromise) return _abraInitPromise + + _abraInitPromise = _init('abra_wasm/abra_wasm_bg.wasm') + return _abraInitPromise +} + +export async function typecheck(input: string): Promise { + await init() + + return Abra.typecheck(input) +} + +export async function run(input: string): Promise { + await init() + + return Abra.runAsync(input) +} + +export async function disassemble(input: string): Promise { + await init() + + return Abra.disassemble(input) +} diff --git a/src/components/CodeMirrorEditor.tsx b/src/components/CodeMirrorEditor.tsx index 978c180..f6bb6d7 100644 --- a/src/components/CodeMirrorEditor.tsx +++ b/src/components/CodeMirrorEditor.tsx @@ -4,6 +4,8 @@ import { UnControlled as CodeMirror } from 'react-codemirror2' import { getSettings, saveSettings } from '../util/local-storage' import playButton from '../img/ui-play.svg' import resetButton from '../img/refresh.svg' +import { run, typecheck } from '../abra-lang/wrapper' +import * as codemirror from 'codemirror' import 'codemirror/addon/edit/closebrackets' import 'codemirror/addon/edit/matchbrackets' import 'codemirror/addon/dialog/dialog' @@ -15,68 +17,143 @@ import '../abra-lang/abra-mode' interface Props { value: string, - onRun: (code: string) => Promise + onCheck: (error: 'wasm' | string | null) => void, + onRun: (result: any, error: string | null, code: string) => void } -export default function Editor({ value, onRun }: Props) { - const [running, setRunning] = React.useState(false) - const [code, setCode] = React.useState(value) - const [useVimMode, setUseVimMode] = React.useState(getSettings().vimModeEnabled) - - // Reset code when props.value changes - React.useEffect(() => void setCode(value), [value]) - - return ( - <> - - setCode(value)} - /> - - - - - - - - - ) +interface State { + isRunning: boolean, + isTypecheckError: boolean, + code: string, + useVimMode: boolean +} + +const codeMirrorOpts = { + mode: 'abra', + theme: 'material', + lineNumbers: true, + lineWrapping: true, + autoCloseBrackets: true, + matchBrackets: true, + autofocus: true +} + +export default class AbraEditor extends React.Component { + handle = 0 + marks: codemirror.TextMarker[] = [] + + constructor(props: Props) { + super(props) + + this.state = { + isRunning: false, + isTypecheckError: false, + code: props.value, + useVimMode: getSettings().vimModeEnabled + } + } + + componentDidUpdate(prevProps: Props, prevState: State) { + if (this.props.value !== prevProps.value) { + this.setState({ code: this.props.value }) + } + } + + run = async () => { + const { code } = this.state + + this.setState({ isRunning: true }) + + try { + const result = await run(code) + if (result.success) { + const { data } = result + this.props.onRun(data, null, code) + } else { + this.props.onRun(null, result.errorMessage.trimStart(), code) + } + } catch (e) { + console.error(e) + this.props.onRun(null, 'Something went wrong, check console', code) + } finally { + this.setState({ isRunning: false }) + } + } + + typecheck = async (editor: codemirror.Editor, code: string) => { + try { + const result = await typecheck(code) + if (result && !result.success) { + this.setState({ isTypecheckError: true }) + this.props.onCheck(result.errorMessage.trimStart()) + + if (result.error.range) { + const { start, end } = result.error.range + const from = { line: start[0] - 1, ch: start[1] - 1 } + const to = { line: end[0] - 1, ch: end[1] } + const mark = editor.getDoc().markText(from, to, { className: 'error-underline' }) + this.marks.push(mark) + } + } else { + this.props.onCheck(null) + } + } catch { + this.props.onCheck('wasm') + } + } + + onChange = (editor: codemirror.Editor, _: codemirror.EditorChange, value: string) => { + this.marks.forEach(mark => mark.clear()) + + this.setState({ isTypecheckError: false, code: value }) + + clearTimeout(this.handle) + this.handle = setTimeout(() => this.typecheck(editor, value), 1000) + } + + render() { + const { value } = this.props + const { code, useVimMode, isRunning, isTypecheckError } = this.state + + return ( + <> + + + + + + + + + + + ) + } } const Controls = styled.aside` @@ -104,7 +181,7 @@ const Button = styled.button` outline: none; border-radius: 2px; background: transparent; - transition: background-color 200ms ease-in-out; + transition: all 200ms ease-in-out; color: white; font-size: 12px; line-height: 12px; @@ -115,6 +192,7 @@ const Button = styled.button` &:disabled { cursor: not-allowed; + opacity: 0.75; &:hover { background: transparent; @@ -125,6 +203,7 @@ const Button = styled.button` const Label = styled.label` color: white; font-size: 12px; + display: flex; ` const IconImg = styled.img` @@ -137,5 +216,10 @@ const CodeMirrorStyles = createGlobalStyle` border-top-left-radius: 6px; border-top-right-radius: 6px; padding-top: 12px; + + .error-underline { + text-decoration: underline red; + background: #ff000077; + } } ` diff --git a/src/components/Tabs.tsx b/src/components/Tabs.tsx index 602de0a..c827d8d 100644 --- a/src/components/Tabs.tsx +++ b/src/components/Tabs.tsx @@ -43,7 +43,7 @@ const TabsList = styled.ul` const TabItem = styled.li` font-size: 14px; - padding: 12px 18px; + padding: 6px 12px; cursor: pointer; background-color: white; transition: all 300ms ease-in-out; diff --git a/src/pages/TryItOutPage.tsx b/src/pages/TryItOutPage.tsx deleted file mode 100644 index f2536c7..0000000 --- a/src/pages/TryItOutPage.tsx +++ /dev/null @@ -1,175 +0,0 @@ -import * as React from 'react' -import styled from 'styled-components' -import init, { runAsync, disassemble } from 'abra_wasm' -import { Section } from '../components/Layout' -import CodeMirrorEditor from '../components/CodeMirrorEditor' -import Dropdown from 'react-dropdown' -import 'react-dropdown/style.css' -import Tabs from '../components/Tabs' -import Code from '../components/Code' - -type Example = 'greeting' | 'fibonacci' | 'fizzbuzz' - -const codeExamples: Record = { - 'greeting': `val greeting = "Hello" - -func greet(recipient: String) = greeting + ", " + recipient - -val languageName = "Abra" -greet(languageName) -`, - 'fibonacci': `func fib(n: Int): Int { - if (n == 0) { - 0 - } else if (n == 1) { - 1 - } else { - fib(n - 2) + fib(n - 1) - } -} - -fib(10)`, - 'fizzbuzz': `// Hint: Check the console for println outputs! - -for a in range(1, 101) { - if a % 15 == 0 { - println("Fizzbuzz") - } else if a % 3 == 0 { - println("Fizz") - } else if a % 5 == 0 { - println("Buzz") - } else { - println(a) - } -}` -} - -const ExternalLink = ({ href, children }: { href: string, children: string }) => - {children} - -let abraInitPromise: Promise | null = null - -export default function TryItOutPage() { - const [output, setOutput] = React.useState(Results will appear when code is run) - const [disassembled, setDisassembled] = React.useState('; The compiled bytecode will be displayed here when the "Run code" button is pressed') - const [example, setExample] = React.useState('fizzbuzz') - - const runCode = React.useCallback(async (code: string) => { - try { - if (!abraInitPromise) { - abraInitPromise = init('abra_wasm/abra_wasm_bg.wasm') - } - await abraInitPromise - } catch (err) { - setOutput( - There was an error initializing the abra wasm module. Please verify that your - browser supports webassembly, or - try again. - ) - console.error(err) - return - } - - const result = await runAsync(code) - if (Array.isArray(result)) { - setOutput({`[${result.join(', ')}]`}) - } else { - setOutput({result}) - } - - const output = disassemble(code) - if (output && output.success) { - setDisassembled(output.disassembled) - } else { - setDisassembled('There was an error initializing the abra wasm module. Please verify that your browser supports webassembly, or try again.') - } - }, []) - - return ( -
-

Try It Out

-

- Press the 'Run code' button below to execute the example code. Results will appear in the result box beneath the - editor. You can make any changes you wish to the code in the editor and run it, but keep in mind that the - language is still very much a work-in-progress, and bugs / unexpected results may arise. -

-

- Please also note that errors of any kind (syntax, type, etc) will currently not be surfaced in this - editor. That feature is Coming Soon ™. Also note that any output of - the println builtin will be sent to the console, so be sure to check there. -

- - - setExample(value as Example)} - /> - - - - - ( - <> -

Note: Output of println will appear in the console, not in this results box

- - > {output} - - - ) - }, - { - title: 'Bytecode', - renderContents: () => ( - <> - - {disassembled} - - - ) - } - ]} - /> - -

What's going on here?

-

- This works because the entire language toolchain (lexer, parser, typechecker, compiler, and virtual machine) is - compiled to WebAssembly. The same code that powers - the native compiler is also running here, with minor amounts of glue code needed to bridge the gap. -

-
- ) -} - -const ResultsView = styled.code` - flex: 1; - background-color: #2B2B2B; - border-radius: 6px; - color: white; - padding: 24px; - font-size: 12px; - max-height: 100px; - overflow: scroll; - - a, a:visited { - color: lightskyblue; - text-decoration: underline; - } -` - -const ExampleDropdownContainer = styled.div` - margin-bottom: 12px; - - label { - font-weight: bold; - margin-right: 12px; - } -` diff --git a/src/pages/TryItOutPage/examples/fibonacci.ts b/src/pages/TryItOutPage/examples/fibonacci.ts new file mode 100644 index 0000000..a1e8e55 --- /dev/null +++ b/src/pages/TryItOutPage/examples/fibonacci.ts @@ -0,0 +1,13 @@ +export default ` +func fib(n: Int): Int { + if (n == 0) { + 0 + } else if (n == 1) { + 1 + } else { + fib(n - 2) + fib(n - 1) + } +} + +fib(12) +`.trimStart() \ No newline at end of file diff --git a/src/pages/TryItOutPage/examples/fizzbuzz.ts b/src/pages/TryItOutPage/examples/fizzbuzz.ts new file mode 100644 index 0000000..6136ae3 --- /dev/null +++ b/src/pages/TryItOutPage/examples/fizzbuzz.ts @@ -0,0 +1,14 @@ +export default ` +// Hint: Check the console for println outputs! + +for a in range(1, 101) { + if a % 15 == 0 { + println("Fizzbuzz") + } else if a % 3 == 0 { + println("Fizz") + } else if a % 5 == 0 { + println("Buzz") + } else { + println(a) + } +}`.trimStart() \ No newline at end of file diff --git a/src/pages/TryItOutPage/examples/greeting.ts b/src/pages/TryItOutPage/examples/greeting.ts new file mode 100644 index 0000000..6bea34b --- /dev/null +++ b/src/pages/TryItOutPage/examples/greeting.ts @@ -0,0 +1,8 @@ +export default ` +val greeting = "Hello" + +func greet(recipient: String) = greeting + ", " + recipient + +val languageName = "Abra" +greet(languageName) +`.trimStart() \ No newline at end of file diff --git a/src/pages/TryItOutPage/examples/linked-list.ts b/src/pages/TryItOutPage/examples/linked-list.ts new file mode 100644 index 0000000..3271e2f --- /dev/null +++ b/src/pages/TryItOutPage/examples/linked-list.ts @@ -0,0 +1,43 @@ +export default ` +// We don't have generics yet, so this LinkedList will only work for Strings + +type Node { + value: String + next: Node? = None +} + +type LinkedList { + head: Node? = None + + func push(self, item: String): LinkedList { + if self.head |head| { + var node = head + while node.next |n| { node = n } + node.next = Node(value: item) + } else { + self.head = Node(value: item) + } + + self + } + + func forEach(self, fn: (String) => Unit): Unit { + var node = self.head + while node |n| { + fn(n.value) + node = n.next + } + } +} + +val list = LinkedList() + .push("a") + .push("b") + .push("c") + .push("d") + +// Iterate through the LinkedList, and push items into an array +var arr = [] +list.forEach(item => arr.push(item)) +arr +`.trimStart() \ No newline at end of file diff --git a/src/pages/TryItOutPage/index.tsx b/src/pages/TryItOutPage/index.tsx new file mode 100644 index 0000000..ad41110 --- /dev/null +++ b/src/pages/TryItOutPage/index.tsx @@ -0,0 +1,161 @@ +import * as React from 'react' +import styled from 'styled-components' +import Dropdown from 'react-dropdown' +import 'react-dropdown/style.css' +import { disassemble } from '../../abra-lang/wrapper' +import { Section } from '../../components/Layout' +import CodeMirrorEditor from '../../components/CodeMirrorEditor' +import Tabs from '../../components/Tabs' +import Code from '../../components/Code' +import greeting from './examples/greeting' +import fibonacci from './examples/fibonacci' +import fizzbuzz from './examples/fizzbuzz' +import linkedList from './examples/linked-list' + +type Example = 'greeting' | 'fibonacci' | 'fizzbuzz' | 'linked-list' + +const codeExamples: Record = { + 'greeting': greeting, + 'fibonacci': fibonacci, + 'fizzbuzz': fizzbuzz, + 'linked-list': linkedList +} + +const ExternalLink = ({ href, children }: { href: string, children: string }) => + {children} + +const noWasmMessage = ( + + There was an error initializing the abra wasm module. Please verify that your + browser supports webassembly, or + try again. + +) + +export default function TryItOutPage() { + const [output, setOutput] = React.useState(Results will appear when code is run) + const [isError, setIsError] = React.useState(false) + const [disassembled, setDisassembled] = React.useState('; The compiled bytecode will be displayed here when the "Run code" button is pressed') + const [example, setExample] = React.useState('greeting') + + const handleNoWasm = React.useCallback(() => { + setOutput(noWasmMessage) + setDisassembled('There was an error initializing the abra wasm module. Please verify that your browser supports webassembly, or try again.') + }, []) + + const onCheckCode = React.useCallback(async (error: 'wasm' | string | null) => { + if (error === 'wasm') return handleNoWasm + + const output = error ?
{error}
: + setOutput(output) + setIsError(!!error) + }, [handleNoWasm]) + + const onRunCode = React.useCallback(async (result: any, error: string | null, code: string) => { + const output = error ?
{error}
: {JSON.stringify(result)} + setOutput(output) + + const disassembly = await disassemble(code) + if (disassembly && disassembly.success) { + setDisassembled(disassembly.disassembled) + } + }, []) + + return ( +
+ + Try It Out + <ExampleDropdownContainer> + <label>Example</label> + <div style={{ width: 200 }}> + <Dropdown + className="dropdown" + value={example} + options={[ + { value: 'greeting', label: 'Basic Greeting' }, + { value: 'fibonacci', label: 'Fibonacci' }, + { value: 'fizzbuzz', label: 'Fizzbuzz' }, + { value: 'linked-list', label: 'Linked List (ish)' } + ]} + onChange={({ value }) => { + setExample(value as Example) + setOutput(<span/>) + setIsError(false) + }} + /> + </div> + </ExampleDropdownContainer> + + + + + ( + + {!isError && '> '}{output} + + ) + }, + { + title: 'Bytecode', + renderContents: () => ( + + {disassembled} + + ) + } + ]} + /> + +

What's going on here?

+

+ This works because the entire language toolchain (lexer, parser, typechecker, compiler, and virtual machine) is + compiled to WebAssembly. The same code that powers + the native compiler is also running here, with minor amounts of glue code needed to bridge the gap. +

+
+ ) +} + +const Title = styled.h1` + display: flex; + justify-content: space-between; + align-items: center; +` + +const ResultsView = styled.code` + flex: 1; + background-color: #2B2B2B; + border-radius: 6px; + color: white; + padding: 16px; + max-height: 100px; + overflow: scroll; + margin: 1em 0; + border: 1px solid #2B2B2B; + font-size: 13px; + + a, a:visited { + color: lightskyblue; + text-decoration: underline; + } +` + +const ExampleDropdownContainer = styled.div` + margin-bottom: 12px; + font-size: 16px; + display: flex; + align-items: center; + + label { + font-weight: bold; + margin-right: 12px; + } +`