-
Notifications
You must be signed in to change notification settings - Fork 106
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Separate program values from their representations (#1651)
* Prepare scheme files for new parser * update JS version for js-slang * proper formatting of files * fix separate program environments across REPL eval calls * remove logger messages from interpreter * Enable variadic continuations for future * Remove Infinity and NaN representation from Scheme * Change scm-slang to follow forked version * update scm-slang to newest parser * resolve linting problems * add test cases to verify proper chapter validation, decoded representation * update scm-slang * Move scheme-specific tests to scm-slang * make scheme test names more obvious * Revert "Move scheme-specific tests to scm-slang" This reverts commit 42e184e. * move scm-slang to dedicated alt-lang folder * remove duplicate code between scm-slang and js-slang * ignore alt langs coverage * update scm-slang * start to add mapping functions for data * update python and scheme-slang * destructively change data types in js-slang, especially since they are not needed in encoded form * prevent js-slang from testing alternate languages - they should manage themselves * add mapping and language-specific representations for the result type * change the command-line REPL to use representations if necessary * add tests for mapper back into coverage pattern * fix arrays being treated as pairs in scheme * add test for mapper * undo accidental deletion of scheme parser tests * fix typo in repl, make undefined check explicit * test every version of scheme parser * resolved issue that caused js-slang to ignore tests * add tests for scheme mapper * Merge remote-tracking branch 'source/master' into master * Add name and parameter data to builtin functions * Repair representation of closures and builtin functions * update scm-slang * update scm-slang * update scm-slang * bump scm-slang * add dummy prelude for scheme --------- Co-authored-by: Martin Henz <[email protected]>
- Loading branch information
1 parent
3fccd1c
commit ea7aee9
Showing
13 changed files
with
424 additions
and
160 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
import { mockContext } from "../../mocks/context"; | ||
import { Chapter, Finished } from "../../types"; | ||
import { mapResult } from "../mapper"; | ||
|
||
test("given source, mapper should do nothing (no mapping needed)", () => { | ||
const context = mockContext(); | ||
const result = { | ||
status: "finished", | ||
context: context, | ||
value: 5, | ||
} as Finished; | ||
const mapper = mapResult(context); | ||
expect(mapper(result)).toEqual(result); | ||
}) | ||
|
||
test("given scheme, mapper should map result to scheme representation", () => { | ||
const context = mockContext(Chapter.SCHEME_1); | ||
const result = { | ||
status: "finished", | ||
context: context, | ||
value: [1, 2, 3, 4, 5], | ||
} as Finished; | ||
const mapper = mapResult(context); | ||
expect(mapper(result)).toEqual({ | ||
status: "finished", | ||
context: context, | ||
value: [1, 2, 3, 4, 5], | ||
representation: { | ||
representation: "#(1 2 3 4 5)", | ||
}, | ||
}); | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
/** | ||
* A generic mapper for all languages. | ||
* If required, maps the final result produced by js-slang to | ||
* the required representation for the language. | ||
*/ | ||
|
||
import { Context, Result } from ".." | ||
import { Chapter } from "../types" | ||
import { mapErrorToScheme, mapResultToScheme } from "./scheme/scheme-mapper" | ||
|
||
/** | ||
* A representation of a value in a language. | ||
* This is used to represent the final value produced by js-slang. | ||
* It is separate from the actual value of the result. | ||
*/ | ||
export class Representation { | ||
constructor(public representation: string) {} | ||
toString() { | ||
return this.representation | ||
} | ||
} | ||
|
||
export function mapResult(context: Context): (x: Result) => Result { | ||
switch (context.chapter) { | ||
case Chapter.SCHEME_1: | ||
case Chapter.SCHEME_2: | ||
case Chapter.SCHEME_3: | ||
case Chapter.SCHEME_4: | ||
case Chapter.FULL_SCHEME: | ||
return x => { | ||
if (x.status === 'finished') { | ||
return mapResultToScheme(x) | ||
} else if (x.status === "error") { | ||
context.errors = context.errors.map(mapErrorToScheme) | ||
} | ||
return x | ||
} | ||
default: | ||
// normally js-slang. | ||
// there is no need for a mapper in this case. | ||
return x => x | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { schemeVisualise } from "../scheme-mapper" | ||
import { make_number } from "../scm-slang/src/stdlib/core-math" | ||
import { circular$45$list, cons, cons$42$, list } from "../scm-slang/src/stdlib/base" | ||
|
||
test("schemeVisualise: should visualise null properly", () => { | ||
expect(schemeVisualise(null).toString()).toEqual("()") | ||
}) | ||
|
||
test("schemeVisualise: should visualise undefined properly", () => { | ||
expect(schemeVisualise(undefined).toString()).toEqual("undefined") | ||
}) | ||
|
||
test("schemeVisualise: should visualise strings properly", () => { | ||
expect(schemeVisualise("hello").toString()).toEqual("\"hello\"") | ||
}) | ||
|
||
test("schemeVisualise: should visualise scheme numbers properly", () => { | ||
expect(schemeVisualise(make_number("1i")).toString()).toEqual("0+1i") | ||
}) | ||
|
||
test("schemeVisualise: should visualise booleans properly", () => { | ||
expect(schemeVisualise(true).toString()).toEqual("#t") | ||
expect(schemeVisualise(false).toString()).toEqual("#f") | ||
}) | ||
|
||
test("schemeVisualise: should visualise circular lists properly", () => { | ||
const circularList = circular$45$list(1, 2, 3) | ||
//expect(schemeVisualise(circularList).toString()).toEqual("#0=(1 2 3 . #0#)") | ||
//for now, this will do | ||
expect(schemeVisualise(circularList).toString()).toEqual("(circular list)") | ||
}) | ||
|
||
test("schemeVisualise: should visualise dotted lists properly", () => { | ||
const dottedList = cons$42$(1, 2, 3) | ||
expect(schemeVisualise(dottedList).toString()).toEqual("(1 2 . 3)") | ||
}) | ||
|
||
test("schemeVisualise: should visualise proper lists properly", () => { | ||
const properList = list(1, 2, 3, 4) | ||
expect(schemeVisualise(properList).toString()).toEqual("(1 2 3 4)") | ||
}) | ||
|
||
test("schemeVisualise: should visualise vectors properly", () => { | ||
const vector = [1, 2, 3, 4] | ||
expect(schemeVisualise(vector).toString()).toEqual("#(1 2 3 4)") | ||
}) | ||
|
||
test("schemeVisualise: should visualise pairs properly", () => { | ||
const pair = cons(1, 2) | ||
expect(schemeVisualise(pair).toString()).toEqual("(1 . 2)") | ||
}) | ||
|
||
test("schemeVisualise: vectors and pairs should be distinct", () => { | ||
const maybe_pair = [1, 2] | ||
expect(schemeVisualise(maybe_pair).toString()).toEqual("#(1 2)") | ||
}) | ||
|
||
export { schemeVisualise } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,192 @@ | ||
import { ArrowFunctionExpression, Identifier, RestElement } from "estree" | ||
import Closure from "../../cse-machine/closure" | ||
import { decode, estreeDecode } from "./scm-slang/src" | ||
import { boolean$63$, car, cdr, circular$45$list$63$, cons, dotted$45$list$63$, last$45$pair, list$45$tail, null$63$, number$63$, pair$63$, proper$45$list$63$, set$45$cdr$33$, vector$63$ } from "./scm-slang/src/stdlib/source-scheme-library" | ||
import { ErrorType, Result, SourceError } from "../../types" | ||
import { List, Pair } from "../../stdlib/list" | ||
import { Representation } from "../mapper" | ||
|
||
export function mapResultToScheme(res: Result): Result { | ||
if (res.status === "finished" || res.status === "suspended-non-det") { | ||
return { | ||
...res, | ||
value: decodeValue(res.value), | ||
representation: showSchemeData(res.value) | ||
} | ||
} | ||
return res | ||
} | ||
|
||
// Given an error, decode its message if and | ||
// only if an encoded value may exist in it. | ||
export function mapErrorToScheme(error: SourceError): SourceError { | ||
if (error.type === ErrorType.SYNTAX) { | ||
// Syntax errors are not encoded. | ||
return error | ||
} | ||
const newExplain = decodeString(error.explain()) | ||
const newElaborate = decodeString(error.elaborate()) | ||
return { | ||
...error, | ||
explain: () => newExplain, | ||
elaborate: () => newElaborate | ||
} | ||
} | ||
|
||
export function showSchemeData(data: any): Representation { | ||
return schemeVisualise(decodeValue(data)) | ||
} | ||
|
||
function decodeString(str: string): string { | ||
return str.replace(/\$scheme_[\w$]+|\$\d+\$/g, match => { | ||
return decode(match) | ||
}) | ||
} | ||
|
||
// Given any value, change the representation of it to | ||
// the required scheme representation. | ||
export function schemeVisualise(x: any): Representation { | ||
// hack: builtins are represented using an object with a toString method | ||
// and minArgsNeeded. | ||
// so to detect these, we use a function that checks for these | ||
function isBuiltinFunction(x: any): boolean { | ||
return x.minArgsNeeded !== undefined && x.toString !== undefined | ||
} | ||
function stringify(x: any): string { | ||
if (null$63$(x)) { | ||
return '()' | ||
} else if (x === undefined) { | ||
return 'undefined' | ||
} else if (typeof x === 'string') { | ||
return `"${x}"` | ||
} else if (number$63$(x)) { | ||
return x.toString() | ||
} else if (boolean$63$(x)) { | ||
return x ? '#t' : '#f' | ||
} else if (x instanceof Closure) { | ||
const node = x.originalNode | ||
const parameters = node.params.map( | ||
(param: Identifier | RestElement) => param.type === "Identifier" | ||
? param.name | ||
: ". " + (param.argument as Identifier).name) | ||
.join(' ') | ||
.trim() | ||
return `#<procedure (${parameters})>` | ||
} else if (isBuiltinFunction(x) || typeof x === 'function') { | ||
function decodeParams(params: string[]): string { | ||
// if parameter starts with ... then it is a rest parameter | ||
const convertedparams = params | ||
.map(param => { | ||
if (param.startsWith('...')) { | ||
return `. ${param.slice(3)}` | ||
} | ||
return param | ||
}) | ||
.map(decodeString) | ||
return convertedparams.join(' ') | ||
} | ||
// take the name and parameter out of the defined function name | ||
const name = decodeString(x.funName) | ||
const parameters = decodeParams(x.funParameters) | ||
return `#<builtin-procedure ${name} (${parameters})>` | ||
} else if (circular$45$list$63$(x)) { | ||
return '(circular list)' | ||
} else if (dotted$45$list$63$(x) && pair$63$(x)) { | ||
let string = '(' | ||
let current = x | ||
while (pair$63$(current)) { | ||
string += `${schemeVisualise(car(current))} ` | ||
current = cdr(current) | ||
} | ||
return string.trim() + ` . ${schemeVisualise(current)})` | ||
} else if (proper$45$list$63$(x)) { | ||
let string = '(' | ||
let current = x | ||
while (current !== null) { | ||
string += `${schemeVisualise(car(current))} ` | ||
current = cdr(current) | ||
} | ||
return string.trim() + ')' | ||
} else if (vector$63$(x)) { | ||
let string = '#(' | ||
for (let i = 0; i < x.length; i++) { | ||
string += `${schemeVisualise(x[i])} ` | ||
} | ||
return string.trim() + ')' | ||
} else { | ||
return x.toString() | ||
} | ||
} | ||
|
||
// return an object with a toString method that returns the stringified version of x | ||
return new Representation(stringify(x)) | ||
} | ||
|
||
// Given any value, decode it if and | ||
// only if an encoded value may exist in it. | ||
// this function is used to accurately display | ||
// values in the REPL. | ||
export function decodeValue(x: any): any { | ||
// helper version of list_tail that assumes non-null return value | ||
function list_tail(xs: List, i: number): List { | ||
if (i === 0) { | ||
return xs | ||
} else { | ||
return list_tail(list$45$tail(xs), i - 1) | ||
} | ||
} | ||
|
||
if (circular$45$list$63$(x)) { | ||
// May contain encoded strings. | ||
let circular_pair_index = -1 | ||
const all_pairs: Pair<any, any>[] = [] | ||
|
||
// iterate through all pairs in the list until we find the circular pair | ||
let current = x | ||
while (current !== null) { | ||
if (all_pairs.includes(current)) { | ||
circular_pair_index = all_pairs.indexOf(current) | ||
break | ||
} | ||
all_pairs.push(current) | ||
current = cdr(current) | ||
} | ||
|
||
// assemble a new list using the elements in all_pairs | ||
let new_list = null | ||
for (let i = all_pairs.length - 1; i >= 0; i--) { | ||
new_list = cons(decodeValue(car(all_pairs[i])), new_list) | ||
} | ||
|
||
// finally we can set the last cdr of the new list to the circular-pair itself | ||
|
||
const circular_pair = list_tail(new_list, circular_pair_index) | ||
set$45$cdr$33$(last$45$pair(new_list), circular_pair) | ||
return new_list | ||
} else if (pair$63$(x)) { | ||
// May contain encoded strings. | ||
return cons(decodeValue(car(x)), decodeValue(cdr(x))) | ||
} else if (vector$63$(x)) { | ||
// May contain encoded strings. | ||
return x.map(decodeValue) | ||
} else if (x instanceof Closure) { | ||
const newNode = estreeDecode(x.originalNode) as ArrowFunctionExpression | ||
|
||
// not a big fan of mutation, but we assert we will never need the original node again anyway | ||
x.node = newNode | ||
x.originalNode = newNode | ||
return x | ||
} else if (typeof x === 'function') { | ||
// copy x to avoid modifying the original object | ||
const newX = { ...x } | ||
const newString = decodeString(x.toString()) | ||
// change the toString method to return the decoded string | ||
newX.toString = () => newString | ||
return newX | ||
} else { | ||
// string, number, boolean, null, undefined | ||
// no need to decode. | ||
return x | ||
} | ||
} | ||
|
Oops, something went wrong.