Skip to content

Commit

Permalink
Separate program values from their representations (#1651)
Browse files Browse the repository at this point in the history
* 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
Kyriel Abad and martin-henz authored Apr 12, 2024
1 parent 3fccd1c commit ea7aee9
Show file tree
Hide file tree
Showing 13 changed files with 424 additions and 160 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
"testRegex": "/__tests__/.*\\.ts$",
"testPathIgnorePatterns": [
"/dist/",
"/src/alt-langs/scheme/scm-slang",
".*benchmark.*",
"/__tests__/(.*/)?utils\\.ts"
],
Expand All @@ -114,7 +115,7 @@
"/node_modules/",
"/src/typings/",
"/src/utils/testing.ts",
"/src/alt-langs",
"/src/alt-langs/scheme/scm-slang",
"/src/py-slang/"
],
"reporters": [
Expand Down
32 changes: 32 additions & 0 deletions src/alt-langs/__tests__/mapper.ts
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)",
},
});
})
43 changes: 43 additions & 0 deletions src/alt-langs/mapper.ts
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
}
}
4 changes: 2 additions & 2 deletions src/alt-langs/scheme/__tests__/scheme-encode-decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { UnassignedVariable } from '../../../errors/errors'
import { decode, encode } from '../scm-slang/src'
import { cons, set$45$cdr$33$ } from '../scm-slang/src/stdlib/base'
import { dummyExpression } from '../../../utils/ast/dummyAstCreator'
import { decodeError, decodeValue } from '../../../parser/scheme'
import { mapErrorToScheme, decodeValue } from '../scheme-mapper'

describe('Scheme encoder and decoder', () => {
it('encoder and decoder are proper inverses of one another', () => {
Expand Down Expand Up @@ -62,6 +62,6 @@ describe('Scheme encoder and decoder', () => {
const token = `😀`
const dummyNode: Node = dummyExpression()
const error = new UnassignedVariable(encode(token), dummyNode)
expect(decodeError(error).elaborate()).toContain(`😀`)
expect(mapErrorToScheme(error).elaborate()).toContain(`😀`)
})
})
58 changes: 58 additions & 0 deletions src/alt-langs/scheme/__tests__/scheme-mapper.ts
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 }
192 changes: 192 additions & 0 deletions src/alt-langs/scheme/scheme-mapper.ts
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
}
}

Loading

0 comments on commit ea7aee9

Please sign in to comment.