diff --git a/src/alt-langs/mapper.ts b/src/alt-langs/mapper.ts index ffb2c5713..ed07063d9 100644 --- a/src/alt-langs/mapper.ts +++ b/src/alt-langs/mapper.ts @@ -40,4 +40,11 @@ export function mapResult(context: Context): (x: Result) => Result { // there is no need for a mapper in this case. return x => x } -} \ No newline at end of file +} + +export const isSchemeLanguage = (context: Context) => + context.chapter === Chapter.SCHEME_1 || + context.chapter === Chapter.SCHEME_2 || + context.chapter === Chapter.SCHEME_3 || + context.chapter === Chapter.SCHEME_4 || + context.chapter === Chapter.FULL_SCHEME \ No newline at end of file diff --git a/src/alt-langs/scheme/scm-slang b/src/alt-langs/scheme/scm-slang index 23d97f8c8..908574c57 160000 --- a/src/alt-langs/scheme/scm-slang +++ b/src/alt-langs/scheme/scm-slang @@ -1 +1 @@ -Subproject commit 23d97f8c81bd0930570eaa26e8585cbaeb8e389c +Subproject commit 908574c579487e33ef7230a8d037e3ad25c778e8 diff --git a/src/createContext.ts b/src/createContext.ts index 2f5ab7dca..623c21db9 100644 --- a/src/createContext.ts +++ b/src/createContext.ts @@ -38,6 +38,8 @@ import { makeWrapper } from './utils/makeWrapper' import * as operators from './utils/operators' import { stringify } from './utils/stringify' import { schemeVisualise } from './alt-langs/scheme/scheme-mapper' +import { cset_apply, cset_eval } from './cse-machine/scheme-macros' +import { Transformers } from './cse-machine/interpreter' export class LazyBuiltIn { func: (...arg0: any) => any @@ -117,6 +119,7 @@ const createEmptyRuntime = () => ({ nodes: [], control: null, stash: null, + transformers: new Transformers(), objectCount: 0, envSteps: -1, envStepsTotal: 0, @@ -450,16 +453,14 @@ export const importBuiltins = (context: Context, externalBuiltIns: CustomBuiltIn if (context.chapter <= +Chapter.SCHEME_1 && context.chapter >= +Chapter.FULL_SCHEME) { switch (context.chapter) { case Chapter.FULL_SCHEME: + // Introduction to eval + // eval metaprocedure + defineBuiltin(context, '$scheme_ZXZhbA$61$$61$(xs)', cset_eval) + case Chapter.SCHEME_4: // Introduction to call/cc defineBuiltin(context, 'call$47$cc(f)', call_with_current_continuation) - // Introduction to eval - - // Scheme apply - // ^ is needed in Schemes 2 and 3 to apply to call functions with rest parameters, - // so we move it there. - case Chapter.SCHEME_3: // Introduction to mutable values, streams @@ -497,10 +498,6 @@ export const importBuiltins = (context: Context, externalBuiltIns: CustomBuiltIn defineBuiltin(context, 'list$45$$62$vector(xs)', scheme_libs.list$45$$62$vector) case Chapter.SCHEME_2: - // Splicing builtin resolvers - // defineBuiltin(context, '$36$make$45$splice(expr)', scheme_libs.make_splice) - // defineBuiltin(context, '$36$resolve$45$splice(xs)', scheme_libs.resolve_splice) - // Scheme pairs defineBuiltin(context, 'cons(left, right)', scheme_libs.cons) defineBuiltin(context, 'xcons(right, left)', scheme_libs.xcons) @@ -622,11 +619,16 @@ export const importBuiltins = (context: Context, externalBuiltIns: CustomBuiltIn defineBuiltin(context, 'list$45$$62$string(xs)', scheme_libs.list$45$$62$string) // Scheme apply is needed here to help in the definition of the Scheme Prelude. - defineBuiltin(context, 'apply(f, ...args)', scheme_libs.apply, 2) + defineBuiltin(context, 'apply(f, ...args)', cset_apply, 2) case Chapter.SCHEME_1: // Display - defineBuiltin(context, 'display(val)', (val: any) => display(schemeVisualise(val))) + defineBuiltin( + context, + 'display(val, prepend = undefined)', + (val: any, ...str: string[]) => display(schemeVisualise(val), ...str), + 1 + ) defineBuiltin(context, 'newline()', () => display('')) // I/O diff --git a/src/cse-machine/__tests__/__snapshots__/cset-machine-apply.ts.snap b/src/cse-machine/__tests__/__snapshots__/cset-machine-apply.ts.snap new file mode 100644 index 000000000..e640dd35d --- /dev/null +++ b/src/cse-machine/__tests__/__snapshots__/cset-machine-apply.ts.snap @@ -0,0 +1,84 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`eval of strings: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval \\"hello\\") + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": "hello", + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`incorrect use of apply throws error (insufficient arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (apply) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Expected 2 arguments, but got 0.", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`incorrect use of apply throws error (last argument not a list): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (apply + 1 2 3) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Last argument of apply must be a list", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`multi-operand apply: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (define args '(1 2 3 4 5)) + (apply + 6 7 8 9 10 args) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 55n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`two-operand apply: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (define args '(1 2)) + (apply + args) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 3n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; diff --git a/src/cse-machine/__tests__/__snapshots__/cset-machine-eval.ts.snap b/src/cse-machine/__tests__/__snapshots__/cset-machine-eval.ts.snap new file mode 100644 index 000000000..1d4858bf9 --- /dev/null +++ b/src/cse-machine/__tests__/__snapshots__/cset-machine-eval.ts.snap @@ -0,0 +1,224 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`eval of application: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(+ 1 2)) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 3n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of begin: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(begin 1 2 3)) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 3n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of booleans: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval #t) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": true, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of define: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define x 1)) + x + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 1n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of empty list: expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '()) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Cannot evaluate null", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`eval of if: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(if #t 1 2)) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 1n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of lambda: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(lambda (x) x)) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": [Function], + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of numbers: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval 1) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 1n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of quote: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(quote (1 2 3))) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": Array [ + SchemeInteger { + "numberType": 1, + "value": 1n, + }, + Array [ + SchemeInteger { + "numberType": 1, + "value": 2n, + }, + Array [ + SchemeInteger { + "numberType": 1, + "value": 3n, + }, + null, + ], + ], + ], + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of set!: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (define x 2) + (eval '(set! x 1)) + x + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 1n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of strings: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval \\"hello\\") + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": "hello", + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`eval of symbols: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (define hello 1) + (eval 'hello) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 1n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; diff --git a/src/cse-machine/__tests__/__snapshots__/cset-machine-macros.ts.snap b/src/cse-machine/__tests__/__snapshots__/cset-machine-macros.ts.snap new file mode 100644 index 000000000..44a337150 --- /dev/null +++ b/src/cse-machine/__tests__/__snapshots__/cset-machine-macros.ts.snap @@ -0,0 +1,97 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`definition of a macro: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (define-syntax my-let + (syntax-rules () + ((_ ((var expr) ...) body ...) + ((lambda (var ...) body ...) expr ...)))) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": undefined, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`failed usage of a macro (no matching pattern): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (define-syntax my-let + (syntax-rules () + ((_ ((var expr) ...) body ...) + ((lambda (var ...) body ...) expr ...)))) + (my-let ((x 1) y) + (+ x y)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: No matching transformer found for macro my-let", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`use of a macro: expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " + (define-syntax my-let + (syntax-rules () + ((_ ((var expr) ...) body ...) + ((lambda (var ...) body ...) expr ...)))) + (my-let ((x 1) (y 2)) + (+ x y)) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 3n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; + +exports[`use of a more complex macro (recursive): expectResult 1`] = ` +Object { + "alertResult": Array [], + "code": " +(define-syntax define-match + (syntax-rules () + ; vars is a pair + ((_ (front . rest) val) + (begin + (if (not (pair? val)) + (error \\"define-match: vars and vals do not match\\")) + (define-match front (car val)) + (define-match rest (cdr val)))) + ; vars is nil + ((_ () val) + ; do nothing + (if #f #f)) + ; vars is a single symbol + ((_ sym val) + (define sym val)))) + (define-match ((x y) z) '((1 2) 3)) + (+ x y z) + ", + "displayResult": Array [], + "numErrors": 0, + "parsedErrors": "", + "result": SchemeInteger { + "numberType": 1, + "value": 6n, + }, + "resultStatus": "finished", + "visualiseListResult": Array [], +} +`; diff --git a/src/cse-machine/__tests__/__snapshots__/cset-machine.ts.snap b/src/cse-machine/__tests__/__snapshots__/cset-machine.ts.snap new file mode 100644 index 000000000..fc7301ef5 --- /dev/null +++ b/src/cse-machine/__tests__/__snapshots__/cset-machine.ts.snap @@ -0,0 +1,376 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`evaluating a poorly formed begin throws error (insufficient arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(begin)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: begin requires at least 1 argument!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define throws error (attempt to redefine special form): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define (if x y) 4)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Cannot shadow special form if with a definition!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define throws error (ill formed define-function): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define (x 1 2 3) 4)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Invalid arguments for lambda!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define throws error (insufficient arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: define requires at least 2 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define throws error (too many arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define x 1 2 3)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: define requires 2 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (attempt to shadow special form): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax if 4)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Cannot shadow special form if with a macro!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (insufficient arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: define-syntax requires 2 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (list is not syntax-rules): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax x (foo bar))) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: define-syntax requires a syntax-rules transformer!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (no syntax-rules list): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax x 1)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: define-syntax requires a syntax-rules transformer!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (syntax is not a symbol): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax 1 4)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: define-syntax requires a symbol!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (syntax-rules has non-list rules): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax x (syntax-rules (x) 1))) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Invalid syntax-rules rule!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (syntax-rules has non-symbol literals): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax x (syntax-rules (1 2) (1 1)))) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Invalid syntax-rules literals!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (syntax-rules has poor literals list): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax x (syntax-rules x (1 1)))) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Invalid syntax-rules literals!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (syntax-rules too few arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax x (syntax-rules))) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: syntax-rules requires at least 2 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed define-syntax throws error (too many arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(define-syntax x 1 2 3)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: define-syntax requires 2 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed if throws error (insufficient arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(if)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: if requires at least 2 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed if throws error (too many arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(if #t 1 2 3)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: if requires at most 3 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed lambda throws error: expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(lambda (1 2 3) x)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Invalid arguments for lambda!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed quote throws error (insufficient arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(quote)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: quote requires 1 argument!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed quote throws error (too many arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(quote x y)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: quote requires 1 argument!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed set! throws error (attempt to set! special form): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(set! if 4)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Cannot overwrite special form if with a value!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed set! throws error (insufficient arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(set!)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: set! requires 2 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a poorly formed set! throws error (too many arguments): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(set! x 1 2 3)) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: set! requires 2 arguments!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating a syntax-rules expression (should not exist outside of define-syntax): expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '(syntax-rules (x) (1 1))) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: syntax-rules must only exist within define-syntax!", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; + +exports[`evaluating null throws error: expectParsedError 1`] = ` +Object { + "alertResult": Array [], + "code": " + (eval '()) + ", + "displayResult": Array [], + "numErrors": 1, + "parsedErrors": "Error: Cannot evaluate null", + "result": undefined, + "resultStatus": "error", + "visualiseListResult": Array [], +} +`; diff --git a/src/cse-machine/__tests__/continuations.ts b/src/cse-machine/__tests__/continuations.ts index b82e82ff9..ae957fee3 100644 --- a/src/cse-machine/__tests__/continuations.ts +++ b/src/cse-machine/__tests__/continuations.ts @@ -1,5 +1,6 @@ +import { mockContext } from '../../mocks/context' import { Call_cc, Continuation, isCallWithCurrentContinuation } from '../continuations' -import { Control, Stash } from '../interpreter' +import { Control, Stash, Transformers } from '../interpreter' test('call/cc is a singleton', () => { expect(Call_cc.get()).toBe(Call_cc.get()) @@ -15,6 +16,6 @@ test('isCallWithCurrentContinuation works on call/cc only', () => { }) test('Continuation toString', () => { - const cont = new Continuation(new Control(), new Stash(), []) + const cont = new Continuation(mockContext(), new Control(), new Stash(), [], new Transformers()) expect(cont.toString()).toBe('continuation') }) diff --git a/src/cse-machine/__tests__/cse-machine-callcc.ts b/src/cse-machine/__tests__/cse-machine-callcc.ts index f26b52683..2249a1799 100644 --- a/src/cse-machine/__tests__/cse-machine-callcc.ts +++ b/src/cse-machine/__tests__/cse-machine-callcc.ts @@ -2,7 +2,7 @@ import { Chapter, Variant } from '../../types' import { expectParsedError, expectResult } from '../../utils/testing' // Continuation tests for Scheme -const optionECScm = { chapter: Chapter.FULL_SCHEME, variant: Variant.EXPLICIT_CONTROL } +const optionECScm = { chapter: Chapter.SCHEME_4, variant: Variant.EXPLICIT_CONTROL } test('basic call/cc works', () => { return expectResult( diff --git a/src/cse-machine/__tests__/cse-machine-runtime-context.ts b/src/cse-machine/__tests__/cse-machine-runtime-context.ts index f5edfac8b..4c3e48e0a 100644 --- a/src/cse-machine/__tests__/cse-machine-runtime-context.ts +++ b/src/cse-machine/__tests__/cse-machine-runtime-context.ts @@ -5,7 +5,7 @@ import { parse } from '../../parser/parser' import { runCodeInSource } from '../../runner' import { Chapter, RecursivePartial } from '../../types' import { stripIndent } from '../../utils/formatters' -import { Control, Stash, generateCSEMachineStateStream } from '../interpreter' +import { Control, Transformers, Stash, generateCSEMachineStateStream } from '../interpreter' const getContextFrom = async (code: string, steps?: number) => { const context = mockContext(Chapter.SOURCE_4) @@ -24,6 +24,7 @@ const evaluateCode = (code: string) => { context.runtime.isRunning = true context.runtime.control = new Control(program as es.Program) context.runtime.stash = new Stash() + context.runtime.transformers = new Transformers() const CSEState = generateCSEMachineStateStream( context, diff --git a/src/cse-machine/__tests__/cset-machine-apply.ts b/src/cse-machine/__tests__/cset-machine-apply.ts new file mode 100644 index 000000000..c13c7b93f --- /dev/null +++ b/src/cse-machine/__tests__/cset-machine-apply.ts @@ -0,0 +1,62 @@ +import { Chapter, Variant } from '../../types' +import { expectParsedError, expectResult } from '../../utils/testing' + +// apply tests for Scheme +const optionECScm = { chapter: Chapter.FULL_SCHEME, variant: Variant.EXPLICIT_CONTROL } + +test('two-operand apply', () => { + return expectResult( + ` + (define args '(1 2)) + (apply + args) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 3n, + } + `) +}) + +test('multi-operand apply', () => { + return expectResult( + ` + (define args '(1 2 3 4 5)) + (apply + 6 7 8 9 10 args) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 55n, + } + `) +}) + +test('eval of strings', () => { + return expectResult( + ` + (eval "hello") + `, + optionECScm + ).toMatchInlineSnapshot(`"hello"`) +}) + +test('incorrect use of apply throws error (insufficient arguments)', () => { + return expectParsedError( + ` + (apply) + `, + optionECScm + ).toMatchInlineSnapshot(`"Expected 2 arguments, but got 0."`) +}) + +test('incorrect use of apply throws error (last argument not a list)', () => { + return expectParsedError( + ` + (apply + 1 2 3) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Last argument of apply must be a list"`) +}) diff --git a/src/cse-machine/__tests__/cset-machine-eval.ts b/src/cse-machine/__tests__/cset-machine-eval.ts new file mode 100644 index 000000000..95c1ef4a9 --- /dev/null +++ b/src/cse-machine/__tests__/cset-machine-eval.ts @@ -0,0 +1,172 @@ +import { Chapter, Variant } from '../../types' +import { expectParsedError, expectResult } from '../../utils/testing' + +// CSET tests for Scheme +const optionECScm = { chapter: Chapter.FULL_SCHEME, variant: Variant.EXPLICIT_CONTROL } + +test('eval of numbers', () => { + return expectResult( + ` + (eval 1) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 1n, + } + `) +}) + +test('eval of booleans', () => { + return expectResult( + ` + (eval #t) + `, + optionECScm + ).toMatchInlineSnapshot(`true`) +}) + +test('eval of strings', () => { + return expectResult( + ` + (eval "hello") + `, + optionECScm + ).toMatchInlineSnapshot(`"hello"`) +}) + +test('eval of symbols', () => { + return expectResult( + ` + (define hello 1) + (eval 'hello) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 1n, + } + `) +}) + +test('eval of empty list', () => { + return expectParsedError( + ` + (eval '()) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Cannot evaluate null"`) +}) + +test('eval of define', () => { + return expectResult( + ` + (eval '(define x 1)) + x + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 1n, + } + `) +}) + +test('eval of lambda', () => { + return expectResult( + ` + (eval '(lambda (x) x)) + `, + optionECScm + ).toMatchInlineSnapshot(`[Function]`) +}) + +test('eval of if', () => { + return expectResult( + ` + (eval '(if #t 1 2)) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 1n, + } + `) +}) + +test('eval of begin', () => { + return expectResult( + ` + (eval '(begin 1 2 3)) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 3n, + } + `) +}) + +test('eval of set!', () => { + return expectResult( + ` + (define x 2) + (eval '(set! x 1)) + x + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 1n, + } + `) +}) + +test('eval of application', () => { + return expectResult( + ` + (eval '(+ 1 2)) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 3n, + } + `) +}) + +test('eval of quote', () => { + return expectResult( + ` + (eval '(quote (1 2 3))) + `, + optionECScm + ).toMatchInlineSnapshot(` + Array [ + SchemeInteger { + "numberType": 1, + "value": 1n, + }, + Array [ + SchemeInteger { + "numberType": 1, + "value": 2n, + }, + Array [ + SchemeInteger { + "numberType": 1, + "value": 3n, + }, + null, + ], + ], + ] + `) +}) diff --git a/src/cse-machine/__tests__/cset-machine-macros.ts b/src/cse-machine/__tests__/cset-machine-macros.ts new file mode 100644 index 000000000..751c2cc83 --- /dev/null +++ b/src/cse-machine/__tests__/cset-machine-macros.ts @@ -0,0 +1,81 @@ +import { Chapter, Variant } from '../../types' +import { expectParsedError, expectResult } from '../../utils/testing' + +// CSET tests for Scheme Macros +const optionECScm = { chapter: Chapter.FULL_SCHEME, variant: Variant.EXPLICIT_CONTROL } + +test('definition of a macro', () => { + return expectResult( + ` + (define-syntax my-let + (syntax-rules () + ((_ ((var expr) ...) body ...) + ((lambda (var ...) body ...) expr ...)))) + `, + optionECScm + ).toMatchInlineSnapshot(`undefined`) +}) + +test('use of a macro', () => { + return expectResult( + ` + (define-syntax my-let + (syntax-rules () + ((_ ((var expr) ...) body ...) + ((lambda (var ...) body ...) expr ...)))) + (my-let ((x 1) (y 2)) + (+ x y)) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 3n, + } + `) +}) + +test('use of a more complex macro (recursive)', () => { + return expectResult( + ` +(define-syntax define-match + (syntax-rules () + ; vars is a pair + ((_ (front . rest) val) + (begin + (if (not (pair? val)) + (error "define-match: vars and vals do not match")) + (define-match front (car val)) + (define-match rest (cdr val)))) + ; vars is nil + ((_ () val) + ; do nothing + (if #f #f)) + ; vars is a single symbol + ((_ sym val) + (define sym val)))) + (define-match ((x y) z) '((1 2) 3)) + (+ x y z) + `, + optionECScm + ).toMatchInlineSnapshot(` + SchemeInteger { + "numberType": 1, + "value": 6n, + } + `) +}) + +test('failed usage of a macro (no matching pattern)', () => { + return expectParsedError( + ` + (define-syntax my-let + (syntax-rules () + ((_ ((var expr) ...) body ...) + ((lambda (var ...) body ...) expr ...)))) + (my-let ((x 1) y) + (+ x y)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: No matching transformer found for macro my-let"`) +}) diff --git a/src/cse-machine/__tests__/cset-machine.ts b/src/cse-machine/__tests__/cset-machine.ts new file mode 100644 index 000000000..aaeedf3ee --- /dev/null +++ b/src/cse-machine/__tests__/cset-machine.ts @@ -0,0 +1,231 @@ +import { Chapter, Variant } from '../../types' +import { expectParsedError } from '../../utils/testing' + +// CSET tests for Scheme (mostly error testing for +// the runtime verification of scheme syntax forms.) +const optionECScm = { chapter: Chapter.FULL_SCHEME, variant: Variant.EXPLICIT_CONTROL } + +test('evaluating null throws error', () => { + return expectParsedError( + ` + (eval '()) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Cannot evaluate null"`) +}) + +test('evaluating a poorly formed lambda throws error', () => { + return expectParsedError( + ` + (eval '(lambda (1 2 3) x)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Invalid arguments for lambda!"`) +}) + +test('evaluating a poorly formed define throws error (insufficient arguments)', () => { + return expectParsedError( + ` + (eval '(define)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: define requires at least 2 arguments!"`) +}) + +test('evaluating a poorly formed define throws error (too many arguments)', () => { + return expectParsedError( + ` + (eval '(define x 1 2 3)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: define requires 2 arguments!"`) +}) + +test('evaluating a poorly formed define throws error (ill formed define-function)', () => { + return expectParsedError( + ` + (eval '(define (x 1 2 3) 4)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Invalid arguments for lambda!"`) +}) + +test('evaluating a poorly formed define throws error (attempt to redefine special form)', () => { + return expectParsedError( + ` + (eval '(define (if x y) 4)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Cannot shadow special form if with a definition!"`) +}) + +test('evaluating a poorly formed set! throws error (insufficient arguments)', () => { + return expectParsedError( + ` + (eval '(set!)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: set! requires 2 arguments!"`) +}) + +test('evaluating a poorly formed set! throws error (too many arguments)', () => { + return expectParsedError( + ` + (eval '(set! x 1 2 3)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: set! requires 2 arguments!"`) +}) + +test('evaluating a poorly formed set! throws error (attempt to set! special form)', () => { + return expectParsedError( + ` + (eval '(set! if 4)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Cannot overwrite special form if with a value!"`) +}) + +test('evaluating a poorly formed if throws error (insufficient arguments)', () => { + return expectParsedError( + ` + (eval '(if)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: if requires at least 2 arguments!"`) +}) + +test('evaluating a poorly formed if throws error (too many arguments)', () => { + return expectParsedError( + ` + (eval '(if #t 1 2 3)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: if requires at most 3 arguments!"`) +}) + +test('evaluating a poorly formed begin throws error (insufficient arguments)', () => { + return expectParsedError( + ` + (eval '(begin)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: begin requires at least 1 argument!"`) +}) + +test('evaluating a poorly formed quote throws error (insufficient arguments)', () => { + return expectParsedError( + ` + (eval '(quote)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: quote requires 1 argument!"`) +}) + +test('evaluating a poorly formed quote throws error (too many arguments)', () => { + return expectParsedError( + ` + (eval '(quote x y)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: quote requires 1 argument!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (insufficient arguments)', () => { + return expectParsedError( + ` + (eval '(define-syntax)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: define-syntax requires 2 arguments!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (too many arguments)', () => { + return expectParsedError( + ` + (eval '(define-syntax x 1 2 3)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: define-syntax requires 2 arguments!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (syntax is not a symbol)', () => { + return expectParsedError( + ` + (eval '(define-syntax 1 4)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: define-syntax requires a symbol!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (attempt to shadow special form)', () => { + return expectParsedError( + ` + (eval '(define-syntax if 4)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Cannot shadow special form if with a macro!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (no syntax-rules list)', () => { + return expectParsedError( + ` + (eval '(define-syntax x 1)) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: define-syntax requires a syntax-rules transformer!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (list is not syntax-rules)', () => { + return expectParsedError( + ` + (eval '(define-syntax x (foo bar))) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: define-syntax requires a syntax-rules transformer!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (syntax-rules too few arguments)', () => { + return expectParsedError( + ` + (eval '(define-syntax x (syntax-rules))) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: syntax-rules requires at least 2 arguments!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (syntax-rules has poor literals list)', () => { + return expectParsedError( + ` + (eval '(define-syntax x (syntax-rules x (1 1)))) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Invalid syntax-rules literals!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (syntax-rules has non-symbol literals)', () => { + return expectParsedError( + ` + (eval '(define-syntax x (syntax-rules (1 2) (1 1)))) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Invalid syntax-rules literals!"`) +}) + +test('evaluating a poorly formed define-syntax throws error (syntax-rules has non-list rules)', () => { + return expectParsedError( + ` + (eval '(define-syntax x (syntax-rules (x) 1))) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: Invalid syntax-rules rule!"`) +}) + +test('evaluating a syntax-rules expression (should not exist outside of define-syntax)', () => { + return expectParsedError( + ` + (eval '(syntax-rules (x) (1 1))) + `, + optionECScm + ).toMatchInlineSnapshot(`"Error: syntax-rules must only exist within define-syntax!"`) +}) diff --git a/src/cse-machine/__tests__/patterns.ts b/src/cse-machine/__tests__/patterns.ts new file mode 100644 index 000000000..798a011f6 --- /dev/null +++ b/src/cse-machine/__tests__/patterns.ts @@ -0,0 +1,53 @@ +import { match } from '../patterns' +import { _Symbol } from '../../alt-langs/scheme/scm-slang/src/stdlib/base' + +function makeList(...args: any[]) { + return args.reduceRight((acc, x) => { + return [x, acc] + }, null) +} + +test('match works on exactly equal items', () => { + const result = match(1, 1, []) + expect(result).toEqual(true) +}) + +test('match works on exactly equal lists', () => { + const result = match(makeList(1, 2, 3), makeList(1, 2, 3), []) + expect(result).toEqual(true) +}) + +test('match works on exactly equal improper lists', () => { + const result = match([1, 2], [1, 2], []) + expect(result).toEqual(true) +}) + +test('match works on a symbol', () => { + const result = match(makeList(1, 2, 3), makeList(1, new _Symbol('x'), 3), []) + expect(result).toEqual(true) +}) + +test('match works on a symbol with anything', () => { + const result = match(makeList(1, 2, 3), new _Symbol('x'), []) + expect(result).toEqual(true) +}) + +test('match works on a pair (improper list)', () => { + const result = match(makeList(1, 2, 3), [new _Symbol('head'), new _Symbol('tail')], []) + expect(result).toEqual(true) +}) + +test('match fails when input does not match literal pattern', () => { + const result = match(makeList(1, 2, 3), 4, []) + expect(result).toEqual(false) +}) + +test('match fails when input does not match syntax literal', () => { + const result = match(makeList(1, 2, 3), new _Symbol('x'), ['x']) + expect(result).toEqual(false) +}) + +test('match fails when input does not match list', () => { + const result = match(1, makeList(1, 2, 3), []) + expect(result).toEqual(false) +}) diff --git a/src/cse-machine/closure.ts b/src/cse-machine/closure.ts index 42dc64784..0d6217a5f 100644 --- a/src/cse-machine/closure.ts +++ b/src/cse-machine/closure.ts @@ -3,6 +3,7 @@ import * as es from 'estree' import { currentEnvironment, + currentTransformers, hasReturnStatement, isBlockStatement, isStatementSequence, @@ -10,7 +11,7 @@ import { } from '../cse-machine/utils' import { Context, Environment, StatementSequence, Value } from '../types' import * as ast from '../utils/ast/astCreator' -import { Control, Stash, generateCSEMachineStateStream } from './interpreter' +import { Control, Transformers, Stash, generateCSEMachineStateStream } from './interpreter' import { envInstr } from './instrCreator' const closureToJS = (value: Closure, context: Context) => { @@ -37,8 +38,12 @@ const closureToJS = (value: Closure, context: Context) => { newContext.runtime.control = new Control() // Also need the env instruction to return back to the current environment at the end. // The call expression won't create one as there is only one item in the control. - newContext.runtime.control.push(envInstr(currentEnvironment(context), node), node) + newContext.runtime.control.push( + envInstr(currentEnvironment(context), currentTransformers(context), node), + node + ) newContext.runtime.stash = new Stash() + newContext.runtime.transformers = context.runtime.transformers const gen = generateCSEMachineStateStream( newContext, newContext.runtime.control, @@ -84,6 +89,7 @@ export default class Closure extends Callable { public static makeFromArrowFunction( node: es.ArrowFunctionExpression, environment: Environment, + transformers: Transformers, context: Context, dummyReturn?: boolean, predefined?: boolean @@ -104,6 +110,7 @@ export default class Closure extends Callable { const closure = new Closure( ast.blockArrowFunction(node.params as es.Identifier[], functionBody, node.loc), environment, + transformers, context, predefined ) @@ -135,6 +142,7 @@ export default class Closure extends Callable { constructor( public node: es.ArrowFunctionExpression, public environment: Environment, + public transformers: Transformers, context: Context, isPredefined?: boolean ) { diff --git a/src/cse-machine/continuations.ts b/src/cse-machine/continuations.ts index 5d0b1f888..09b06d7fd 100644 --- a/src/cse-machine/continuations.ts +++ b/src/cse-machine/continuations.ts @@ -1,7 +1,38 @@ import * as es from 'estree' -import { Environment } from '../types' -import { Control, Stash } from './interpreter' +import { Context, Environment } from '../types' +import { Control, Stash, Transformers } from './interpreter' +import { uniqueId } from './utils' + +/** + * A dummy function used to detect for the apply function object. + * If the interpreter sees this specific function, it applies the function + * with the given arguments to apply. + * + * We need this to be a metaprocedure so that it can properly handle + * the arguments passed to it, even if they are continuations. + */ +export class Apply extends Function { + private static instance: Apply = new Apply() + + private constructor() { + super() + } + + public static get(): Apply { + return Apply.instance + } + + public toString(): string { + return 'apply' + } +} + +export const apply = Apply.get() + +export function isApply(value: any): boolean { + return value === apply +} /** * A dummy function used to detect for the call/cc function object. @@ -42,12 +73,24 @@ export class Continuation extends Function { private control: Control private stash: Stash private env: Environment[] + private transformers: Transformers - constructor(control: Control, stash: Stash, env: Environment[]) { + /** Unique ID defined for continuation */ + public readonly id: string + + constructor( + context: Context, + control: Control, + stash: Stash, + env: Environment[], + transformers: Transformers + ) { super() this.control = control.copy() this.stash = stash.copy() this.env = [...env] + this.transformers = transformers + this.id = uniqueId(context) } // As the continuation needs to be immutable (we can call it several times) @@ -64,9 +107,17 @@ export class Continuation extends Function { return [...this.env] } + public getTransformers(): Transformers { + return this.transformers + } + public toString(): string { return 'continuation' } + + public equals(other: Continuation): boolean { + return this === other + } } /** diff --git a/src/cse-machine/instrCreator.ts b/src/cse-machine/instrCreator.ts index c360f00f5..3e29846a0 100644 --- a/src/cse-machine/instrCreator.ts +++ b/src/cse-machine/instrCreator.ts @@ -18,6 +18,7 @@ import { UnOpInstr, WhileInstr } from './types' +import { Transformers } from './interpreter' export const resetInstr = (srcNode: Node): Instr => ({ instrType: InstrType.RESET, @@ -90,9 +91,14 @@ export const branchInstr = ( srcNode }) -export const envInstr = (env: Environment, srcNode: Node): EnvInstr => ({ +export const envInstr = ( + env: Environment, + transformers: Transformers, + srcNode: Node +): EnvInstr => ({ instrType: InstrType.ENVIRONMENT, env, + transformers, srcNode }) diff --git a/src/cse-machine/interpreter.ts b/src/cse-machine/interpreter.ts index 15c98912f..364fe8bbf 100644 --- a/src/cse-machine/interpreter.ts +++ b/src/cse-machine/interpreter.ts @@ -52,6 +52,7 @@ import { createEnvironment, createProgramEnvironment, currentEnvironment, + currentTransformers, declareFunctionsAndVariables, declareIdentifier, defineVariable, @@ -74,9 +75,14 @@ import { popEnvironment, pushEnvironment, reduceConditional, + setTransformers, setVariable, valueProducing } from './utils' +import { isApply, isEval, schemeEval } from './scheme-macros' +import { Transformer } from './patterns' +import { isSchemeLanguage } from '../alt-langs/mapper' +import { flattenList, isList } from './macro-utils' type CmdEvaluator = ( command: ControlItem, @@ -171,6 +177,53 @@ export class Stash extends Stack { } } +/** + * The T component is a dictionary of mappings from syntax names to + * their corresponding syntax rule transformers (patterns). + * + * Similar to the E component, there is a matching + * "T" environment tree that is used to store the transformers. + * as such, we need to track the transformers and update them with the environment. + */ +export class Transformers { + private parent: Transformers | null + private items: Map + public constructor(parent?: Transformers) { + this.parent = parent || null + this.items = new Map() + } + + // only call this if you are sure that the pattern exists. + public getPattern(name: string): Transformer[] { + // check if the pattern exists in the current transformer + if (this.items.has(name)) { + return this.items.get(name) as Transformer[] + } + // else check if the pattern exists in the parent transformer + if (this.parent) { + return this.parent.getPattern(name) + } + // should not get here. use this properly. + throw new Error(`Pattern ${name} not found in transformers`) + } + + public hasPattern(name: string): boolean { + // check if the pattern exists in the current transformer + if (this.items.has(name)) { + return true + } + // else check if the pattern exists in the parent transformer + if (this.parent) { + return this.parent.hasPattern(name) + } + return false + } + + public addPattern(name: string, item: Transformer[]): void { + this.items.set(name, item) + } +} + /** * Function to be called when a program is to be interpreted using * the explicit control evaluator. @@ -192,6 +245,11 @@ export function evaluate(program: es.Program, context: Context, options: IOption context.runtime.isRunning = true context.runtime.control = new Control(program) context.runtime.stash = new Stash() + // set a global transformer if it does not exist. + context.runtime.transformers = context.runtime.transformers + ? context.runtime.transformers + : new Transformers() + return runCSEMachine( context, context.runtime.control, @@ -334,11 +392,11 @@ export function* generateCSEMachineStateStream( // Push first node to be evaluated into context. // The typeguard is there to guarantee that we are pushing a node (which should always be the case) - if (command && isNode(command)) { + if (command !== undefined && isNode(command)) { context.runtime.nodes.unshift(command) } - while (command) { + while (command !== undefined) { // Return to capture a snapshot of the control and stash after the target step count is reached if (!isPrelude && steps === envSteps) { yield { stash, control, steps } @@ -377,9 +435,12 @@ export function* generateCSEMachineStateStream( // With the new evaluator, we don't return a break // return new CSEBreak() } - } else { + } else if (isInstr(command)) { // Command is an instruction cmdEvaluators[command.instrType](command, context, control, stash, isPrelude) + } else { + // this is a scheme value + schemeEval(command, context, control, stash, isPrelude) } // Push undefined into the stack if both control and stash is empty @@ -463,7 +524,13 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { !(isInstr(next) && next.instrType === InstrType.ENVIRONMENT) && !control.canAvoidEnvInstr() ) { - control.push(instr.envInstr(currentEnvironment(context), command)) + control.push( + instr.envInstr( + currentEnvironment(context), + context.runtime.transformers as Transformers, + command + ) + ) } const environment = createBlockEnvironment(context, 'blockEnvironment') @@ -762,6 +829,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { const closure: Closure = Closure.makeFromArrowFunction( command, currentEnvironment(context), + currentTransformers(context), context, true, isPrelude @@ -785,7 +853,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.RESET]: function (command: Instr, context: Context, control: Control, stash: Stash) { // Keep pushing reset instructions until marker is found. const cmdNext: ControlItem | undefined = control.pop() - if (cmdNext && (isNode(cmdNext) || cmdNext.instrType !== InstrType.MARKER)) { + if (cmdNext && (!isInstr(cmdNext) || cmdNext.instrType !== InstrType.MARKER)) { control.push(instr.resetInstr(command.srcNode)) } }, @@ -931,6 +999,49 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { handleRuntimeError(context, new errors.CallingNonFunctionValue(func, command.srcNode)) } + if (isApply(func)) { + // Check for number of arguments mismatch error + checkNumberOfArguments(context, func, args, command.srcNode) + + // get the procedure from the arguments + const proc = args[0] + // get the last list from the arguments + // (and it should be a list) + const last = args[args.length - 1] + if (!isList(last)) { + handleRuntimeError( + context, + new errors.ExceptionError(new Error('Last argument of apply must be a list')) + ) + } + // get the rest of the arguments between the procedure and the last list + const rest = args.slice(1, args.length - 1) + // convert the last list to an array + const lastAsArray = flattenList(last) + // combine the rest and the last list + const combined = [...rest, ...lastAsArray] + + // push the items back onto the stash + stash.push(proc) + stash.push(...combined) + + // prepare a function call for the procedure + control.push(instr.appInstr(combined.length, command.srcNode)) + return + } + + if (isEval(func)) { + // Check for number of arguments mismatch error + checkNumberOfArguments(context, func, args, command.srcNode) + + // get the AST from the arguments + const AST = args[0] + + // move it to the control + control.push(AST) + return + } + if (isCallWithCurrentContinuation(func)) { // Check for number of arguments mismatch error checkNumberOfArguments(context, func, args, command.srcNode) @@ -939,6 +1050,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { const contControl = control.copy() const contStash = stash.copy() const contEnv = context.runtime.environments.slice() + const contTransformers = currentTransformers(context) // at this point, the extra CALL instruction // has been removed from the control stack. @@ -947,9 +1059,15 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { // and additionally, call/cc itself has been removed from the stash. // as such, there is no further need to modify the - // copied C, S and E! - - const continuation = new Continuation(contControl, contStash, contEnv) + // copied C, S, E and T! + + const continuation = new Continuation( + context, + contControl, + contStash, + contEnv, + contTransformers + ) // Get the callee const cont_callee: Value = args[0] @@ -971,26 +1089,17 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { // Check for number of arguments mismatch error checkNumberOfArguments(context, func, args, command.srcNode) - // const dummyContCallExpression = makeDummyContCallExpression('f', 'cont') - - // // Restore the state of the stash, - // // but replace the function application instruction with - // // a resume continuation instruction - // stash.push(func) - // // we need to push the arguments back onto the stash - // // as well - // stash.push(...args) - // control.push(instr.resumeContInstr(command.numOfArgs, dummyContCallExpression)) - // get the C, S, E from the continuation const contControl = func.getControl() const contStash = func.getStash() const contEnv = func.getEnv() + const contTransformers = func.getTransformers() // update the C, S, E of the current context control.setTo(contControl) stash.setTo(contStash) context.runtime.environments = contEnv + setTransformers(context, contTransformers) // push the arguments back onto the stash stash.push(...args) @@ -1006,12 +1115,18 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { // Push ENVIRONMENT instruction if needed - if next control stack item // exists and is not an environment instruction, OR the control only contains // environment indepedent items + // if the current language is a scheme language, don't avoid the environment instruction + // as schemers like using the REPL, and that always assumes that the environment is reset + // to the main environment. if ( - next && - !(isInstr(next) && next.instrType === InstrType.ENVIRONMENT) && - !control.canAvoidEnvInstr() + (next && + !(isInstr(next) && next.instrType === InstrType.ENVIRONMENT) && + !control.canAvoidEnvInstr()) || + isSchemeLanguage(context) ) { - control.push(instr.envInstr(currentEnvironment(context), command.srcNode)) + control.push( + instr.envInstr(currentEnvironment(context), currentTransformers(context), command.srcNode) + ) } // Create environment for function parameters if the function isn't nullary. @@ -1037,6 +1152,9 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(func.node.body) } + // we need to update the transformers environment here. + const newTransformers = new Transformers(func.transformers) + setTransformers(context, newTransformers) return } @@ -1119,6 +1237,8 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { while (currentEnvironment(context).id !== command.env.id) { popEnvironment(context) } + // restore transformers environment + setTransformers(context, command.transformers) }, [InstrType.ARRAY_LITERAL]: function ( diff --git a/src/cse-machine/macro-utils.ts b/src/cse-machine/macro-utils.ts new file mode 100644 index 000000000..195399f09 --- /dev/null +++ b/src/cse-machine/macro-utils.ts @@ -0,0 +1,101 @@ +import { List, Pair } from '../stdlib/list' + +/** + * Low-level check for a list. + * @param value any value + * @returns whether the value is a list + */ +export function isList(value: any): value is List { + if (value === null) { + return true + } + return Array.isArray(value) && value.length === 2 && isList(value[1]) +} + +/** + * Turn a list into an array. + * @param value a list + * @returns + */ +export function flattenList(value: List): any[] { + if (value === null) { + return [] + } + return [value[0], ...flattenList(value[1])] +} + +/** + * Convert an array into a list. + * @param arr + * @returns + */ +export function arrayToList(arr: any[]): List { + return arrayToImproperList(arr, null) as List +} + +/** + * Convert an array into an improper list. + * @param arr + * @param last + * @returns + */ +export function arrayToImproperList(arr: any[], last: any): any { + if (arr.length === 0) { + return last + } + const pair: any[] = [arr[0], arrayToImproperList(arr.slice(1), last)] as any[] + ;(pair as any).pair = true + return pair +} + +/** + * Check if a value is an improper list. + * We force an improper list to be an array of two elements. + * @param value + * @returns + */ +export function isImproperList(value: any): value is Pair { + if (value === null) { + return false + } + return Array.isArray(value) && value.length === 2 && !isList(value[1]) +} + +/** + * Check if a value is a pair. + * @param value + * @returns + */ +export function isPair(value: any): value is Pair { + return Array.isArray(value) && value.length === 2 +} + +/** + * Convert an improper list into an array and a terminator. + * @param value + * @returns + */ +export function flattenImproperList(value: any): [any[], any] { + let items = [] + let working = value + while (working instanceof Array && working.length === 2) { + items.push(working[0]) + working = working[1] + } + return [items, working] +} + +/** + * Get the length of an improper list. + * @param value + * @returns + */ +export function improperListLength(value: any): number { + let length = 0 + let working = value + while (isPair(working)) { + length++ + working = working[1] + } + return length +} diff --git a/src/cse-machine/patterns.ts b/src/cse-machine/patterns.ts new file mode 100644 index 000000000..6ccaa6cec --- /dev/null +++ b/src/cse-machine/patterns.ts @@ -0,0 +1,563 @@ +// a single transformer stored within the transformers component +// will be henceforth referred to as a "transformer". +// it consists of a set of literals used as additional syntax, +// a pattern (for a list to match against) +// and a final template (for the list to be transformed into). +import { List, Pair } from '../stdlib/list' +import { _Symbol } from '../alt-langs/scheme/scm-slang/src/stdlib/base' +import { + arrayToImproperList, + arrayToList, + flattenList, + improperListLength, + isImproperList, + isPair, + isList +} from './macro-utils' +import { atomic_equals, is_number } from '../alt-langs/scheme/scm-slang/src/stdlib/core-math' + +// a single pattern stored within the patterns component +// may have several transformers attributed to it. +export class Transformer { + literals: string[] + pattern: List + template: List + + constructor(literals: string[], pattern: List, template: List) { + this.literals = literals + this.pattern = pattern + this.template = template + } +} + +// given a matching transformer, +// the macro_transform() function will transform a list +// into the template of the transformer. +export function macro_transform(input: any, transformer: Transformer): any { + const collected = collect(input, transformer.pattern, transformer.literals) + return transform(transformer.template, collected) +} + +// we use the match() function to match a list against a pattern and literals +// and verify if it is a match. +export function match(input: any, pattern: any, literals: string[]): boolean { + // we should compare the input and pattern based on the possible forms of pattern: + // 1. an identifier + // 2. a literal such as null, a number, a string, or a boolean + // 3. (+) + // 4. (+ . ) + // 5. (+ ... +) + // 6. (+ ... + . ) + + // case 1 + if (pattern instanceof _Symbol && literals.includes(pattern.sym)) { + return input instanceof _Symbol && input.sym === pattern.sym + } + + if (pattern instanceof _Symbol) { + return !(input instanceof _Symbol && literals.includes(input.sym)) + } + + // case 2 + if (pattern === null) { + return input === null + } + + if (is_number(pattern)) { + return is_number(input) && atomic_equals(input, pattern) + } + + if (typeof pattern === 'string' || typeof pattern === 'boolean' || typeof pattern === 'number') { + return input === pattern + } + + // case 3 and 5 + if (isList(pattern)) { + if (!isList(input)) { + return false + } + const inputList = flattenList(input) + const patternList = flattenList(pattern) + // there can be a single ellepsis in the pattern, but it must be behind some element. + // scan the pattern for the ... symbol. + // we will need the position of the ... symbol to compare the front and back of the list. + const ellipsisIndex = patternList.findIndex( + elem => elem instanceof _Symbol && elem.sym === '...' + ) + + // case 5 + if (ellipsisIndex !== -1) { + // if the input is shorter than the pattern (minus the ... and matching pattern), it can't match. + if (inputList.length < patternList.length - 2) { + return false + } + + const frontPatternLength = ellipsisIndex - 1 + const ellipsisPattern = patternList[ellipsisIndex - 1] + const backPatternLength = patternList.length - ellipsisIndex - 1 + + // compare the front of the list with the front of the pattern as per normal + for (let i = 0; i < frontPatternLength; i++) { + if (!match(inputList[i], patternList[i], literals)) { + return false + } + } + + // compare the items that should be captured by the ellipsis + for (let i = frontPatternLength; i < inputList.length - backPatternLength; i++) { + if (!match(inputList[i], ellipsisPattern, literals)) { + return false + } + } + + // now we can compare the back of the list with the rest of the patterns + for (let i = inputList.length - backPatternLength; i < inputList.length; i++) { + if ( + !match(inputList[i], patternList[i - (inputList.length - patternList.length)], literals) + ) { + return false + } + } + + // else all is good and return true + return true + } + + // case 3 + if (inputList.length !== patternList.length) { + return false + } + for (let i = 0; i < inputList.length; i++) { + if (!match(inputList[i], patternList[i], literals)) { + return false + } + } + return true + } + + // case 4 and 6 + if (isImproperList(pattern)) { + // if the input is not a pair, it can't match. + if (!isPair(input)) { + return false + } + + let currEllipsisPattern + let currentPattern = pattern + let currentInput = input + let ellipsisFound = false + + // iterate through currentPattern while it is a pair + while (isPair(currentPattern)) { + if (!isPair(currentInput)) { + return false + } + const [headPattern, tailPattern] = currentPattern + const [headInput, tailInput] = currentInput + + // we can lookahead to see if the ellipsis symbol is the next pattern element. + if ( + isPair(tailPattern) && + tailPattern[0] instanceof _Symbol && + tailPattern[0].sym === '...' + ) { + ellipsisFound = true + currEllipsisPattern = headPattern + // skip ahead to the (cddr pattern) for the next iteration + // the cddr is what "remains" of the pattern after the ellipsis. + currentPattern = tailPattern[1] + continue + } + + // if the ellipis is found, continue to match the pattern until the ellipsis is exhausted. + // (this is done by comparing the length of the input to the length of the remaining pattern) + if (ellipsisFound && improperListLength(currentInput) > improperListLength(currentPattern)) { + // match the headInput with the currEllipsisPattern + if (!match(headInput, currEllipsisPattern, literals)) { + return false + } + currentInput = tailInput // move to the next input + continue + } + + // if the ellipsis symbol is not found, or we have already matched the ellipsis pattern, + // match the headInput with the headPattern + if (!match(headInput, headPattern, literals)) { + return false + } + currEllipsisPattern = headPattern + currentPattern = tailPattern + currentInput = tailInput + } + // now we can compare the last item in the pattern with the rest of the input + return match(currentInput, currentPattern, literals) + } + + return false +} + +// once a pattern is matched, we need to collect all of the matched variables. +// ONLY called on matching patterns. +export function collect(input: any, pattern: any, literals: string[]): Map { + const collected = new Map() + // we should compare the input and pattern based on the possible forms of pattern: + // 1. an identifier + // 2. a literal such as null, a number, a string, or a boolean + // 3. (+) + // 4. (+ . ) + // 5. (+ ... +) + // 6. (+ ... + . ) + + // case 1 + if (pattern instanceof _Symbol && !literals.includes(pattern.sym)) { + if (!collected.has(pattern.sym)) { + collected.set(pattern.sym, []) + } + collected.get(pattern.sym)?.push(input) + return collected + } + + // case 2 + if (pattern === null) { + return collected + } + + if (is_number(pattern)) { + return collected + } + + if (typeof pattern === 'string' || typeof pattern === 'boolean' || typeof pattern === 'number') { + return collected + } + + // cases 3 and 5 + if (isList(pattern)) { + const inputList = flattenList(input) + const patternList = flattenList(pattern) + const ellipsisIndex = patternList.findIndex( + elem => elem instanceof _Symbol && elem.sym === '...' + ) + + // case 5 + if (ellipsisIndex !== -1) { + const frontPatternLength = ellipsisIndex - 1 + const ellipsisPattern = patternList[ellipsisIndex - 1] + const backPatternLength = patternList.length - ellipsisIndex - 1 + + for (let i = 0; i < frontPatternLength; i++) { + const val = collect(inputList[i], patternList[i], literals) + for (let [key, value] of val) { + if (!collected.has(key)) { + collected.set(key, []) + } + collected.get(key)?.push(...value) + } + } + + for (let i = frontPatternLength; i < inputList.length - backPatternLength; i++) { + const val = collect(inputList[i], ellipsisPattern, literals) + for (let [key, value] of val) { + if (!collected.has(key)) { + collected.set(key, []) + } + collected.get(key)?.push(...value) + } + } + + for (let i = inputList.length - backPatternLength; i < inputList.length; i++) { + const val = collect( + inputList[i], + patternList[i - (inputList.length - patternList.length)], + literals + ) + for (let [key, value] of val) { + if (!collected.has(key)) { + collected.set(key, []) + } + collected.get(key)?.push(...value) + } + } + return collected + } + + // case 3 + for (let i = 0; i < inputList.length; i++) { + const val = collect(inputList[i], patternList[i], literals) + for (let [key, value] of val) { + if (!collected.has(key)) { + collected.set(key, []) + } + collected.get(key)?.push(...value) + } + } + return collected + } + + // case 4 and 6 + if (isImproperList(pattern)) { + let currEllipsisPattern + let currentPattern = pattern + let currentInput = input + let ellipsisFound = false + + // iterate through currentPattern while it is a pair + while (isPair(currentPattern)) { + const [headPattern, tailPattern] = currentPattern + const [headInput, tailInput] = currentInput + + // we can lookahead to see if the ellipsis symbol is the next pattern element. + if ( + isPair(tailPattern) && + tailPattern[0] instanceof _Symbol && + tailPattern[0].sym === '...' + ) { + ellipsisFound = true + currEllipsisPattern = headPattern + // skip ahead to the (cddr pattern) for the next iteration + // the cddr is what "remains" of the pattern after the ellipsis. + currentPattern = tailPattern[1] + continue + } + + // if the ellipis is found, continue to match the pattern until the ellipsis is exhausted. + // (this is done by comparing the length of the input to the length of the remaining pattern) + // it may be the case that the ellipsis pattern is not matched at all. + if (ellipsisFound && improperListLength(currentInput) > improperListLength(currentPattern)) { + const val = collect(headInput, currEllipsisPattern, literals) + for (let [key, value] of val) { + if (!collected.has(key)) { + collected.set(key, []) + } + collected.get(key)?.push(...value) + } + currentInput = tailInput // move to the next input + continue + } + + // if the ellipsis symbol is not found, or we have already matched the ellipsis pattern, + // match the headInput with the headPattern + const val = collect(headInput, headPattern, literals) + for (let [key, value] of val) { + if (!collected.has(key)) { + collected.set(key, []) + } + collected.get(key)?.push(...value) + } + currEllipsisPattern = headPattern + currentPattern = tailPattern + currentInput = tailInput + } + // now we can compare the last item in the pattern with the rest of the input + const val = collect(currentInput, currentPattern, literals) + for (let [key, value] of val) { + if (!collected.has(key)) { + collected.set(key, []) + } + collected.get(key)?.push(...value) + } + return collected + } + + return collected +} + +// when matched against a pattern, we use the transform() function +// to transform the list into the template. +// returns a list, a pair, or any value, as determined by the template. +export function transform( + template: any, + collected: Map, + indexToCollect: number = 0 +): any { + // there are 5 possible forms of the template: + // 1. an identifier + // 2. a literal such as null, a number, a string, or a boolean + // 3. (...