From 60ab4c2ca44cdc3e4ce21c2d3775e7fe2667bbe7 Mon Sep 17 00:00:00 2001 From: Zainab Lawal Date: Thu, 26 Jun 2025 10:03:06 +0300 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20add=20pair=20and=20list=20functions?= =?UTF-8?q?=20for=20Python=20=C2=A72=20(fixes=20#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement pair, head, tail, is_pair for Python §2. --- src/conductor/stdlib/index.ts | 8 +++-- src/conductor/stdlib/list/index.ts | 1 + src/conductor/stdlib/list/pair.ts | 38 +++++++++++++++++++++ src/resolver.ts | 4 +++ src/stdlib.ts | 53 ++++++++++++++++++++++++++++-- src/stdlib/py_s1_constants.json | 6 +++- 6 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 src/conductor/stdlib/list/pair.ts diff --git a/src/conductor/stdlib/index.ts b/src/conductor/stdlib/index.ts index 717a06c..9a2ab5d 100644 --- a/src/conductor/stdlib/index.ts +++ b/src/conductor/stdlib/index.ts @@ -3,12 +3,16 @@ // Original author(s): Source Academy Team import type { StdlibFunction } from "../types"; -import { accumulate, is_list, length } from "./list"; +import { accumulate, is_list, length, pair, is_pair, head, tail } from "./list"; export const stdlib = { is_list: is_list, accumulate: accumulate, - length: length + length: length, + pair: pair, + is_pair: is_pair, + head: head, + tail: tail } satisfies Record>; export { accumulate }; diff --git a/src/conductor/stdlib/list/index.ts b/src/conductor/stdlib/list/index.ts index 86cfee5..089e97d 100644 --- a/src/conductor/stdlib/list/index.ts +++ b/src/conductor/stdlib/list/index.ts @@ -7,3 +7,4 @@ export { is_list } from "./is_list"; export { length } from "./length"; export { list_to_vec } from "./list_to_vec"; export { list } from "./list"; +export { pair, is_pair, head, tail } from "./pair"; diff --git a/src/conductor/stdlib/list/pair.ts b/src/conductor/stdlib/list/pair.ts new file mode 100644 index 0000000..dbe39cc --- /dev/null +++ b/src/conductor/stdlib/list/pair.ts @@ -0,0 +1,38 @@ +import { DataType, IDataHandler, PairIdentifier, TypedValue } from "../../types"; + +/** + * Makes a pair with x as head and y as tail. + * @param x - given head + * @param y - given tail + * @returns pair with x as head and y as tail. + */ +export function pair(this: IDataHandler, x: TypedValue, y: TypedValue): PairIdentifier { + return this.pair_make(x, y); +} + +/** + * @param x - given value + * @returns whether x is a pair + */ +export function is_pair(this: IDataHandler, x: any): boolean { + + return typeof x === 'object' && x !== null && x.__brand === 'pair'; +} + +/** + * Returns the head of given pair p. + * @param p - given pair + * @returns head of p + */ +export function head(this: IDataHandler, p: PairIdentifier): TypedValue { + return this.pair_head(p); +} + +/** + * Returns the tail of given pair p. + * @param p - given pair + * @returns tail of p + */ +export function tail(this: IDataHandler, p: PairIdentifier): TypedValue { + return this.pair_tail(p); +} \ No newline at end of file diff --git a/src/resolver.ts b/src/resolver.ts index 50d4404..8584863 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -166,14 +166,18 @@ export class Resolver implements StmtNS.Visitor, ExprNS.Visitor { ["abs", new Token(TokenType.NAME, "abs", 0, 0, 0)], ["char_at", new Token(TokenType.NAME, "char_at", 0, 0, 0)], ["error", new Token(TokenType.NAME, "error", 0, 0, 0)], + ["head", new Token(TokenType.NAME, "head", 0, 0, 0)], ["input", new Token(TokenType.NAME, "input", 0, 0, 0)], ["isinstance", new Token(TokenType.NAME, "isinstance", 0, 0, 0)], + ["is_pair", new Token(TokenType.NAME, "is_pair", 0, 0, 0)], ["max", new Token(TokenType.NAME, "max", 0, 0, 0)], ["min", new Token(TokenType.NAME, "min", 0, 0, 0)], + ["pair", new Token(TokenType.NAME, "pair", 0, 0, 0)], ["print", new Token(TokenType.NAME, "print", 0, 0, 0)], ["random_random", new Token(TokenType.NAME, "random_random", 0, 0, 0)], ["round", new Token(TokenType.NAME, "round", 0, 0, 0)], ["str", new Token(TokenType.NAME, "str", 0, 0, 0)], + ["tail", new Token(TokenType.NAME, "tail", 0, 0, 0)], ["time_time", new Token(TokenType.NAME, "time_time", 0, 0, 0)], // math constants diff --git a/src/stdlib.ts b/src/stdlib.ts index eab420a..170a058 100644 --- a/src/stdlib.ts +++ b/src/stdlib.ts @@ -1519,11 +1519,60 @@ export class BuiltInFunctions { const result = toPythonString(obj); return { type: 'string', value: result }; } + + /** + * SICP-style pair constructor: returns a pair object with head and tail. + */ + static pair(args: Value[], source: string, command: ControlItem, context: Context): Value { + if (args.length !== 2) { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, 'pair', 'pair expects exactly 2 arguments')); + } + return { type: 'pair', value: { head: args[0], tail: args[1] } }; + } + + /** + * SICP-style is_pair: returns true if x is a pair, false otherwise. + */ + static is_pair(args: Value[], source: string, command: ControlItem, context: Context): Value { + if (args.length !== 1) { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, 'is_pair', 'is_pair expects exactly 1 argument')); + } + const x = args[0]; + return { type: 'bool', value: x.type === 'pair' }; + } + + /** + * SICP-style head: returns the head of a pair. + */ + static head(args: Value[], source: string, command: ControlItem, context: Context): Value { + if (args.length !== 1) { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, 'head', 'head expects exactly 1 argument')); + } + const p = args[0]; + if (!p || p.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, p?.type ?? typeof p, 'pair')); + } + return p.value.head; + } + + /** + * SICP-style tail: returns the tail of a pair. + */ + static tail(args: Value[], source: string, command: ControlItem, context: Context): Value { + if (args.length !== 1) { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, 'tail', 'tail expects exactly 1 argument')); + } + const p = args[0]; + if (!p || p.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, p?.type ?? typeof p, 'pair')); + } + return p.value.tail; + } } import py_s1_constants from './stdlib/py_s1_constants.json'; -// NOTE: If we ever switch to another Python “chapter” (e.g. py_s2_constants), +// NOTE: If we ever switch to another Python "chapter" (e.g. py_s2_constants), // just change the variable below to switch to the set. const constants = py_s1_constants; @@ -1574,7 +1623,7 @@ for (const name of constants.builtInFuncs as string[]) { * https://github.com/python/cpython/blob/main/Python/pystrtod.c * * Special cases such as -0, Infinity, and NaN are also handled to ensure that - * output matches Python’s display conventions. + * output matches Python's display conventions. */ export function toPythonFloat(num: number): string { if (Object.is(num, -0)) { diff --git a/src/stdlib/py_s1_constants.json b/src/stdlib/py_s1_constants.json index 5bd9ac9..77b541a 100644 --- a/src/stdlib/py_s1_constants.json +++ b/src/stdlib/py_s1_constants.json @@ -62,7 +62,11 @@ "time_time", "str", "print", - "input" + "input", + "pair", + "is_pair", + "head", + "tail" ], "constants": [ "math_e", From 7ec802bed4126e4f822984d684ccb22a49272c8f Mon Sep 17 00:00:00 2001 From: Zainab Lawal Date: Mon, 14 Jul 2025 00:56:28 +0300 Subject: [PATCH 2/2] add soure 2 list processing fucntions and tests --- src/conductor/stdlib/index.ts | 8 +- src/conductor/stdlib/list/index.ts | 1 - src/conductor/stdlib/list/list.prelude.ts | 197 ++++++++ src/conductor/stdlib/list/pair.ts | 38 -- src/createContext.ts | 125 +++++ src/cse-machine/context.ts | 1 + src/index.ts | 67 ++- src/modules/loader/loaders.ts | 118 +++++ src/resolver.ts | 4 - src/stdlib.ts | 554 ++++++++++++++++++++-- src/stdlib/index.ts | 179 +++++++ src/stdlib/preludes.ts | 26 + src/stdlib/py_s1_constants.json | 6 +- src/stdlib/py_s2_constants.json | 94 ++++ src/tests/prelude.test.ts | 292 ++++++++++++ src/tests/stdlib.test.ts | 185 ++++++++ 16 files changed, 1804 insertions(+), 91 deletions(-) create mode 100644 src/conductor/stdlib/list/list.prelude.ts delete mode 100644 src/conductor/stdlib/list/pair.ts create mode 100644 src/createContext.ts create mode 100644 src/modules/loader/loaders.ts create mode 100644 src/stdlib/index.ts create mode 100644 src/stdlib/preludes.ts create mode 100644 src/stdlib/py_s2_constants.json create mode 100644 src/tests/prelude.test.ts create mode 100644 src/tests/stdlib.test.ts diff --git a/src/conductor/stdlib/index.ts b/src/conductor/stdlib/index.ts index 9a2ab5d..717a06c 100644 --- a/src/conductor/stdlib/index.ts +++ b/src/conductor/stdlib/index.ts @@ -3,16 +3,12 @@ // Original author(s): Source Academy Team import type { StdlibFunction } from "../types"; -import { accumulate, is_list, length, pair, is_pair, head, tail } from "./list"; +import { accumulate, is_list, length } from "./list"; export const stdlib = { is_list: is_list, accumulate: accumulate, - length: length, - pair: pair, - is_pair: is_pair, - head: head, - tail: tail + length: length } satisfies Record>; export { accumulate }; diff --git a/src/conductor/stdlib/list/index.ts b/src/conductor/stdlib/list/index.ts index 089e97d..86cfee5 100644 --- a/src/conductor/stdlib/list/index.ts +++ b/src/conductor/stdlib/list/index.ts @@ -7,4 +7,3 @@ export { is_list } from "./is_list"; export { length } from "./length"; export { list_to_vec } from "./list_to_vec"; export { list } from "./list"; -export { pair, is_pair, head, tail } from "./pair"; diff --git a/src/conductor/stdlib/list/list.prelude.ts b/src/conductor/stdlib/list/list.prelude.ts new file mode 100644 index 0000000..4831f87 --- /dev/null +++ b/src/conductor/stdlib/list/list.prelude.ts @@ -0,0 +1,197 @@ +export const listPrelude = ` +def equal(xs, ys): + """ + Pure Function: Returns true if both have the same structure (pairs) + and identical values at corresponding leaf positions. + """ + if is_pair(xs): + return is_pair(ys) and \ + equal(head(xs), head(ys)) and \ + equal(tail(xs), tail(ys)) + elif is_null(xs): + return is_null(ys) + elif is_number(xs): + return is_number(ys) and xs == ys + elif is_boolean(xs): + return is_boolean(ys) and xs == ys + elif is_string(xs): + return is_string(ys) and xs == ys + elif is_undefined(xs): # Catches None in Python + return is_undefined(ys) + elif is_function(xs): + return is_function(ys) and xs == ys + else: + return xs == ys + +def _length(xs, acc): + """ + Helper for length, designed for Tail Recursion. + """ + return acc if is_null(xs) else _length(tail(xs), acc + 1) + +def length(xs): + """ + Returns the length of the list xs. + """ + return _length(xs, 0) + +def _map(f, xs, acc): + return reverse(acc) if is_null(xs) else _map(f, tail(xs), pair(f(head(xs)), acc)) + +def map(f, xs): + """ + Returns a list that results from list xs by element-wise application of unary function f. + """ + return _map(f, xs, None) + +def _build_list(i, fun, already_built): + return already_built if i < 0 else _build_list(i - 1, fun, pair(fun(i), already_built)) + +def build_list(fun, n): + """ + Makes a list with n elements by applying the unary function fun + to the numbers 0 to n - 1. + """ + return _build_list(n - 1, fun, None) + +def for_each(fun, xs): + """ + Applies the unary function fun to every element of the list xs. + """ + if is_null(xs): + return True + else: + fun(head(xs)) # Side effect happens here if fun is not pure + return for_each(fun, tail(xs)) + +def list_to_string(xs): + """ + Returns a string that represents list xs using the text-based box-and-pointer notation. + """ + return _list_to_string(xs, lambda x: x) + +def _list_to_string(xs, cont): + if is_null(xs): + return cont('null') + elif is_pair(xs): + return _list_to_string(head(xs), lambda x_str: + _list_to_string(tail(xs), lambda y_str: + cont(f'[{x_str},{y_str}]') + )) + else: + return cont(stringify(xs)) + +def list_to_string(xs): + """ + Returns a string that represents list xs using the text-based box-and-pointer notation. + """ + return _list_to_string(xs, lambda x: x) + +def _reverse(original, reversed_acc): + return reversed_acc if is_null(original) else _reverse_iter(tail(original), pair(head(original), reversed_acc)) + +def reverse(xs): + """ + Returns list xs in reverse order. + """ + return _reverse_iter(xs, None) + +def _append(xs, ys, cont): + return cont(ys) if is_null(xs) else _append(tail(xs), ys, lambda zs: cont(pair(head(xs), zs))) + +def append(xs, ys): + """ + Returns a list that results from appending the list ys to the list xs. + """ + return _append(xs, ys, lambda x: x) + +def member(v, xs): + """ + Returns first postfix sublist whose head is identical to v (using ===). + Returns null if the element does not occur in the list. + """ + if is_null(xs): + return None + elif v == head(xs): + return xs + else: + return member(v, tail(xs)) + +def _remove(v, xs, acc): + if is_null(xs): + return append(reverse(acc), xs) + elif v == head(xs): + return append(reverse(acc), tail(xs)) + else: + return _remove(v, tail(xs), pair(head(xs), acc)) + +def remove(v, xs): + """ + Returns a list that results from xs by removing the first item from xs that is identical (===) to v. + """ + return _remove(v, xs, None) + +def _remove_all(v, xs, acc): + if is_null(xs): + return append(reverse(acc), xs) + elif v == head(xs): + return _remove_all(v, tail(xs), acc) + else: + return _remove_all(v, tail(xs), pair(head(xs), acc)) + +def remove_all(v, xs): + """ + Returns a list that results from xs by removing all items from xs that are identical (===) to v. + """ + return _remove_all(v, xs, None) + +def _enum_list(start, end, acc): + return reverse(acc) if start > end else _enum_list(start + 1, end, pair(start, acc)) + +def enum_list(start, end): + """ + Makes a list with elements from start to end (inclusive). + """ + return _enum_list(start, end, None) + +def list_ref(xs, n): + """ + Returns the element of list xs at position n (0-indexed). + """ + if n == 0: + if is_null(xs): raise IndexError("list_ref: index out of bounds on null list") + return head(xs) + else: + if is_null(xs): raise IndexError("list_ref: index out of bounds") + return list_ref(tail(xs), n - 1) + +def _accumulate(f, initial, xs, cont): + if is_null(xs): + return cont(initial) + else: + # Recursive CPS call: Process tail, then apply f with head, then pass to continuation + return _accumulate(f, initial, tail(xs), lambda x_accumulated_from_tail: + cont(f(head(xs), x_accumulated_from_tail))) + +def accumulate(f, initial, xs): + """ + Applies binary function f to the elements of xs from right-to-left order. + """ + return _accumulate(f, initial, xs, lambda x: x) + +def _filter(pred, xs, acc): + if is_null(xs): + return reverse(acc) + else: + if pred(head(xs)): + return _filter(pred, tail(xs), pair(head(xs), acc)) + else: + return _filter(pred, tail(xs), acc) + +def filter(pred, xs): + """ + Returns a list that contains only those elements for which the one-argument + function pred returns true. + """ + return _filter(pred, xs, None) +`; \ No newline at end of file diff --git a/src/conductor/stdlib/list/pair.ts b/src/conductor/stdlib/list/pair.ts deleted file mode 100644 index dbe39cc..0000000 --- a/src/conductor/stdlib/list/pair.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { DataType, IDataHandler, PairIdentifier, TypedValue } from "../../types"; - -/** - * Makes a pair with x as head and y as tail. - * @param x - given head - * @param y - given tail - * @returns pair with x as head and y as tail. - */ -export function pair(this: IDataHandler, x: TypedValue, y: TypedValue): PairIdentifier { - return this.pair_make(x, y); -} - -/** - * @param x - given value - * @returns whether x is a pair - */ -export function is_pair(this: IDataHandler, x: any): boolean { - - return typeof x === 'object' && x !== null && x.__brand === 'pair'; -} - -/** - * Returns the head of given pair p. - * @param p - given pair - * @returns head of p - */ -export function head(this: IDataHandler, p: PairIdentifier): TypedValue { - return this.pair_head(p); -} - -/** - * Returns the tail of given pair p. - * @param p - given pair - * @returns tail of p - */ -export function tail(this: IDataHandler, p: PairIdentifier): TypedValue { - return this.pair_tail(p); -} \ No newline at end of file diff --git a/src/createContext.ts b/src/createContext.ts new file mode 100644 index 0000000..f18c3ce --- /dev/null +++ b/src/createContext.ts @@ -0,0 +1,125 @@ +import { Context } from './cse-machine/context'; +import { importBuiltins } from './stdlib/index'; +import { importPrelude } from './stdlib/preludes'; +import { Chapter } from './stdlib/index'; + +// Re-export Chapter type for external use +export { Chapter }; + +// External builtins that might be loaded from external sources +const externalBuiltIns: Record = { + // Add external builtins here when needed +}; + +// External symbols that might be loaded from modules +const externalSymbols: Record = { + // Add external symbols here when needed +}; + +/** + * Creates a context for a specific chapter and variant + * @param chapter The chapter number (1-4) or 'LIBRARY_PARSER' + * @param variant The variant (not used in current implementation) + * @returns A new Context with all necessary builtins and preludes loaded + */ +export function createContext(chapter: Chapter, variant: string = 'default'): Context { + const context = new Context(); + + // Import builtins for the specific chapter + importBuiltins(context, chapter); + + // Import external builtins + importExternalBuiltIns(context, externalBuiltIns); + + // Import preludes and load them into the environment + const preludeCode = importPrelude(context, typeof chapter === 'number' ? chapter : 4); + if (preludeCode) { + context.prelude = preludeCode; + // Load the prelude code into the environment + loadPreludeIntoEnvironment(context, preludeCode); + } + + // Import external symbols + importExternalSymbols(context, externalSymbols); + + return context; +} + +/** + * Imports external builtins into the context + */ +function importExternalBuiltIns(context: Context, externalBuiltIns: Record) { + for (const [name, func] of Object.entries(externalBuiltIns)) { + context.nativeStorage.builtins.set(name, func); + } +} + +/** + * Imports external symbols into the context + */ +function importExternalSymbols(context: Context, externalSymbols: Record) { + for (const [name, value] of Object.entries(externalSymbols)) { + context.nativeStorage.builtins.set(name, value); + } +} + +/** + * Creates an empty context without any builtins or preludes + * Useful for testing or when you want to load everything manually + */ +export function createEmptyContext(chapter: Chapter, variant: string = 'default'): Context { + return new Context(); +} + +/** + * Loads modules from a server (similar to js-slang's module loading) + * @param context The context to load modules into + * @param serverUrl The URL of the module server + */ +export async function loadModulesFromServer(context: Context, serverUrl: string) { + try { + // This would implement the actual module loading logic + // For now, it's a placeholder + console.log(`Loading modules from ${serverUrl}`); + } catch (error) { + console.error('Failed to load modules from server:', error); + } +} + +/** + * Loads a specific module by name + * @param context The context to load the module into + * @param moduleName The name of the module to load + */ +export async function loadModule(context: Context, moduleName: string) { + try { + // This would implement the actual module loading logic + // For now, it's a placeholder + console.log(`Loading module: ${moduleName}`); + } catch (error) { + console.error(`Failed to load module ${moduleName}:`, error); + } +} + +/** + * Loads prelude code into the environment by parsing and evaluating it + * @param context The context to load the prelude into + * @param preludeCode The prelude code to load + */ +function loadPreludeIntoEnvironment(context: Context, preludeCode: string) { + try { + // Import the necessary modules for parsing and evaluation + const { parsePythonToEstreeAst } = require('./index'); + const { runCSEMachine } = require('./runner/pyRunner'); + + // Parse the prelude code + const preludeAst = parsePythonToEstreeAst(preludeCode, 1, true); + + // Run the prelude code in the context + const result = runCSEMachine(preludeCode, preludeAst, context, { isPrelude: true }); + + console.log('Prelude loaded successfully'); + } catch (error) { + console.error('Failed to load prelude into environment:', error); + } +} \ No newline at end of file diff --git a/src/cse-machine/context.ts b/src/cse-machine/context.ts index 93fb4f0..8e71ccb 100644 --- a/src/cse-machine/context.ts +++ b/src/cse-machine/context.ts @@ -21,6 +21,7 @@ export class Context { public stash: Stash; //public environment: Environment; public errors: CseError[] = []; + public prelude: string | null = null; runtime: { break: boolean diff --git a/src/index.ts b/src/index.ts index 48eaf32..6ffce88 100644 --- a/src/index.ts +++ b/src/index.ts @@ -140,6 +140,9 @@ import { Finished, RecursivePartial, Result } from "./types"; import { runCSEMachine } from "./runner/pyRunner"; import { initialise } from "./conductor/runner/util/initialise"; import { PyEvaluator } from "./conductor/runner/types/PyEvaluator"; +import { createContext, Chapter } from './createContext'; +import * as fs from 'fs'; +import * as path from 'path'; export * from './errors'; export function parsePythonToEstreeAst(code: string, @@ -200,9 +203,71 @@ export async function runInContext( context: Context, options: RecursivePartial = {} ): Promise { + // Load and run the Python prelude first + const preludeCode = loadPythonPrelude(); + if (preludeCode) { + const preludeAst = parsePythonToEstreeAst(preludeCode, 1, true); + await runCSEMachine(preludeCode, preludeAst, context, { ...options, isPrelude: true }); + } + + // Now run the main code const estreeAst = parsePythonToEstreeAst(code, 1, true); const result = runCSEMachine(code, estreeAst, context, options); return result; } -const {runnerPlugin, conduit} = initialise(PyEvaluator); +/** + * Runs Python code in a context with a specific chapter + * @param code The Python code to run + * @param chapter The chapter to use (1-4 or 'LIBRARY_PARSER') + * @param options Additional options + * @returns The result of running the code + */ +export async function runInChapter( + code: string, + chapter: Chapter = 2, + options: RecursivePartial = {} +): Promise { + const context = createContext(chapter); + return runInContext(code, context, options); +} + +/** + * Loads and runs the Python prelude file containing list processing functions + */ +function loadPythonPrelude(): string { + try { + const preludePath = path.join(__dirname, 'conductor', 'stdlib', 'list', 'list.prelude.py'); + return fs.readFileSync(preludePath, 'utf8'); + } catch (error) { + console.warn('Could not load Python prelude:', error); + return ''; + } +} + +if (require.main === module) { + (async () => { + if (process.argv.length < 3) { + console.error("Usage: npm run start:dev -- "); + process.exit(1); + } + + const filePath = process.argv[2]; + + try { + const context = new Context(); + const options = {}; + const code = fs.readFileSync(filePath, "utf8") + "\n"; + console.log(`Parsing Python file: ${filePath}`); + + const result = await runInContext(code, context, options); + console.info(result); + console.info((result as Finished).value); + console.info((result as Finished).representation.toString((result as Finished).value)); + + } catch (e) { + console.error("Error:", e); + } + + })(); +} \ No newline at end of file diff --git a/src/modules/loader/loaders.ts b/src/modules/loader/loaders.ts new file mode 100644 index 0000000..58ce813 --- /dev/null +++ b/src/modules/loader/loaders.ts @@ -0,0 +1,118 @@ +import { Context } from '../../cse-machine/context'; + +// URL for external modules (similar to js-slang) +const MODULES_STATIC_URL = 'https://source-academy.github.io/modules'; + +/** + * Interface for module functions + */ +export interface ModuleFunctions { + [key: string]: any; +} + +/** + * Interface for require provider + */ +export interface RequireProvider { + (moduleName: string): any; +} + +/** + * Loads a module bundle asynchronously + * @param moduleName The name of the module to load + * @param context The context to load the module into + * @returns The loaded module functions + */ +export async function loadModuleBundleAsync( + moduleName: string, + context: Context +): Promise { + try { + const moduleUrl = `${MODULES_STATIC_URL}/bundles/${moduleName}.js`; + + // In a real implementation, this would fetch and execute the module + // For now, we'll return an empty object + console.log(`Loading module bundle from: ${moduleUrl}`); + + // Placeholder implementation + const result = await bundleAndTabImporter(moduleUrl); + return result(getRequireProvider(context)); + } catch (error) { + console.error(`Failed to load module bundle ${moduleName}:`, error); + throw error; + } +} + +/** + * Imports a bundle and tab + * @param url The URL to load from + * @returns A function that takes a require provider and returns module functions + */ +async function bundleAndTabImporter(url: string): Promise<(requireProvider: RequireProvider) => ModuleFunctions> { + // In a real implementation, this would: + // 1. Fetch the JavaScript bundle from the URL + // 2. Execute it in a sandboxed environment + // 3. Return the module functions + + // For now, return a placeholder function + return (requireProvider: RequireProvider) => { + console.log('Bundle importer called with require provider'); + return {}; + }; +} + +/** + * Creates a require provider for the given context + * @param context The context to create the require provider for + * @returns A require provider function + */ +function getRequireProvider(context: Context): RequireProvider { + return (moduleName: string) => { + // Check if module is already loaded + if (context.nativeStorage.loadedModules[moduleName]) { + return context.nativeStorage.loadedModules[moduleName]; + } + + // For now, return undefined for unloaded modules + console.log(`Module ${moduleName} not found in context`); + return undefined; + }; +} + +/** + * Loads module documentation + * @param moduleName The name of the module + * @returns The module documentation + */ +export async function loadModuleDocumentation(moduleName: string): Promise { + try { + const docUrl = `${MODULES_STATIC_URL}/jsons/${moduleName}.json`; + console.log(`Loading module documentation from: ${docUrl}`); + + // In a real implementation, this would fetch the JSON documentation + // For now, return an empty object + return {}; + } catch (error) { + console.error(`Failed to load module documentation for ${moduleName}:`, error); + throw error; + } +} + +/** + * Loads module tabs (UI components) + * @param moduleName The name of the module + * @returns The module tabs + */ +export async function loadModuleTabs(moduleName: string): Promise { + try { + const tabsUrl = `${MODULES_STATIC_URL}/tabs/${moduleName}.js`; + console.log(`Loading module tabs from: ${tabsUrl}`); + + // In a real implementation, this would fetch and execute the tabs + // For now, return an empty object + return {}; + } catch (error) { + console.error(`Failed to load module tabs for ${moduleName}:`, error); + throw error; + } +} \ No newline at end of file diff --git a/src/resolver.ts b/src/resolver.ts index 8584863..50d4404 100644 --- a/src/resolver.ts +++ b/src/resolver.ts @@ -166,18 +166,14 @@ export class Resolver implements StmtNS.Visitor, ExprNS.Visitor { ["abs", new Token(TokenType.NAME, "abs", 0, 0, 0)], ["char_at", new Token(TokenType.NAME, "char_at", 0, 0, 0)], ["error", new Token(TokenType.NAME, "error", 0, 0, 0)], - ["head", new Token(TokenType.NAME, "head", 0, 0, 0)], ["input", new Token(TokenType.NAME, "input", 0, 0, 0)], ["isinstance", new Token(TokenType.NAME, "isinstance", 0, 0, 0)], - ["is_pair", new Token(TokenType.NAME, "is_pair", 0, 0, 0)], ["max", new Token(TokenType.NAME, "max", 0, 0, 0)], ["min", new Token(TokenType.NAME, "min", 0, 0, 0)], - ["pair", new Token(TokenType.NAME, "pair", 0, 0, 0)], ["print", new Token(TokenType.NAME, "print", 0, 0, 0)], ["random_random", new Token(TokenType.NAME, "random_random", 0, 0, 0)], ["round", new Token(TokenType.NAME, "round", 0, 0, 0)], ["str", new Token(TokenType.NAME, "str", 0, 0, 0)], - ["tail", new Token(TokenType.NAME, "tail", 0, 0, 0)], ["time_time", new Token(TokenType.NAME, "time_time", 0, 0, 0)], // math constants diff --git a/src/stdlib.ts b/src/stdlib.ts index 170a058..50ed378 100644 --- a/src/stdlib.ts +++ b/src/stdlib.ts @@ -1520,61 +1520,543 @@ export class BuiltInFunctions { return { type: 'string', value: result }; } - /** - * SICP-style pair constructor: returns a pair object with head and tail. - */ + // ===== LIST FUNCTIONS FOR SOURCE 2 ===== + + @Validate(2, 2, 'pair', false) static pair(args: Value[], source: string, command: ControlItem, context: Context): Value { - if (args.length !== 2) { - handleRuntimeError(context, new TypeError(source, command as es.Node, context, 'pair', 'pair expects exactly 2 arguments')); + const head = args[0]; + const tail = args[1]; + return { + type: 'pair', + head: head, + tail: tail + }; + } + + @Validate(1, 1, 'head', false) + static head(args: Value[], source: string, command: ControlItem, context: Context): Value { + const pair = args[0]; + if (pair.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, pair.type, "pair")); + } + return (pair as any).head; + } + + @Validate(1, 1, 'tail', false) + static tail(args: Value[], source: string, command: ControlItem, context: Context): Value { + const pair = args[0]; + if (pair.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, pair.type, "pair")); } - return { type: 'pair', value: { head: args[0], tail: args[1] } }; + return (pair as any).tail; + } + + @Validate(1, 1, 'is_null', false) + static is_null(args: Value[], source: string, command: ControlItem, context: Context): Value { + const value = args[0]; + return { type: 'bool', value: value.type === 'null' || value.type === 'undefined' }; } - /** - * SICP-style is_pair: returns true if x is a pair, false otherwise. - */ + @Validate(1, 1, 'is_pair', false) static is_pair(args: Value[], source: string, command: ControlItem, context: Context): Value { - if (args.length !== 1) { - handleRuntimeError(context, new TypeError(source, command as es.Node, context, 'is_pair', 'is_pair expects exactly 1 argument')); + const value = args[0]; + return { type: 'bool', value: value.type === 'pair' }; + } + + static list(args: Value[], source: string, command: ControlItem, context: Context): Value { + let result: Value = { type: 'null', value: null }; + + // Build list from right to left + for (let i = args.length - 1; i >= 0; i--) { + result = { + type: 'pair', + head: args[i], + tail: result + }; } - const x = args[0]; - return { type: 'bool', value: x.type === 'pair' }; + + return result; } - /** - * SICP-style head: returns the head of a pair. - */ - static head(args: Value[], source: string, command: ControlItem, context: Context): Value { - if (args.length !== 1) { - handleRuntimeError(context, new TypeError(source, command as es.Node, context, 'head', 'head expects exactly 1 argument')); + @Validate(1, 1, 'length', false) + static length(args: Value[], source: string, command: ControlItem, context: Context): Value { + const list = args[0]; + let count = 0; + let current = list; + + while (current.type === 'pair') { + count++; + current = (current as any).tail; } - const p = args[0]; - if (!p || p.type !== 'pair') { - handleRuntimeError(context, new TypeError(source, command as es.Node, context, p?.type ?? typeof p, 'pair')); + + if (current.type !== 'null' && current.type !== 'undefined') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); } - return p.value.head; + + return { type: 'bigint', value: BigInt(count) }; } - /** - * SICP-style tail: returns the tail of a pair. - */ - static tail(args: Value[], source: string, command: ControlItem, context: Context): Value { - if (args.length !== 1) { - handleRuntimeError(context, new TypeError(source, command as es.Node, context, 'tail', 'tail expects exactly 1 argument')); + @Validate(2, 2, 'map', false) + static map(args: Value[], source: string, command: ControlItem, context: Context): Value { + const func = args[0]; + const list = args[1]; + + if (func.type !== 'closure') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, func.type, "function")); + } + + // Helper function to map over list + const mapHelper = (current: Value): Value => { + if (current.type === 'null' || current.type === 'undefined') { + return { type: 'null', value: null }; + } + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + const head = (current as any).head; + const tail = (current as any).tail; + + // Apply function to head + const mappedHead = (func as any).apply(context, [head]); + + // Recursively map tail + const mappedTail = mapHelper(tail); + + return { + type: 'pair', + head: mappedHead, + tail: mappedTail + }; + }; + + return mapHelper(list); + } + + @Validate(2, 2, 'filter', false) + static filter(args: Value[], source: string, command: ControlItem, context: Context): Value { + const predicate = args[0]; + const list = args[1]; + + if (predicate.type !== 'closure') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, predicate.type, "function")); + } + + const filterHelper = (current: Value): Value => { + if (current.type === 'null' || current.type === 'undefined') { + return { type: 'null', value: null }; + } + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + const head = (current as any).head; + const tail = (current as any).tail; + + // Apply predicate to head + const shouldInclude = (predicate as any).apply(context, [head]); + + // Recursively filter tail + const filteredTail = filterHelper(tail); + + // Include head if predicate returns true + if (shouldInclude.type === 'bool' && shouldInclude.value === true) { + return { + type: 'pair', + head: head, + tail: filteredTail + }; + } else { + return filteredTail; + } + }; + + return filterHelper(list); + } + + @Validate(3, 3, 'accumulate', false) + static accumulate(args: Value[], source: string, command: ControlItem, context: Context): Value { + const func = args[0]; + const initial = args[1]; + const list = args[2]; + + if (func.type !== 'closure') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, func.type, "function")); + } + + const accumulateHelper = (current: Value, acc: Value): Value => { + if (current.type === 'null' || current.type === 'undefined') { + return acc; + } + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + const head = (current as any).head; + const tail = (current as any).tail; + + // Recursively accumulate tail first (right-to-left) + const tailResult = accumulateHelper(tail, acc); + + // Apply function to head and tail result + return (func as any).apply(context, [head, tailResult]); + }; + + return accumulateHelper(list, initial); + } + + @Validate(2, 2, 'append', false) + static append(args: Value[], source: string, command: ControlItem, context: Context): Value { + const list1 = args[0]; + const list2 = args[1]; + + const appendHelper = (current: Value): Value => { + if (current.type === 'null' || current.type === 'undefined') { + return list2; + } + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + const head = (current as any).head; + const tail = (current as any).tail; + + return { + type: 'pair', + head: head, + tail: appendHelper(tail) + }; + }; + + return appendHelper(list1); + } + + @Validate(1, 1, 'reverse', false) + static reverse(args: Value[], source: string, command: ControlItem, context: Context): Value { + const list = args[0]; + + const reverseHelper = (current: Value, acc: Value): Value => { + if (current.type === 'null' || current.type === 'undefined') { + return acc; + } + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + const head = (current as any).head; + const tail = (current as any).tail; + + const newAcc = { + type: 'pair', + head: head, + tail: acc + }; + + return reverseHelper(tail, newAcc); + }; + + return reverseHelper(list, { type: 'null', value: null }); + } + + @Validate(2, 2, 'list_ref', false) + static list_ref(args: Value[], source: string, command: ControlItem, context: Context): Value { + const list = args[0]; + const index = args[1]; + + if (index.type !== 'bigint') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, index.type, "int")); + } + + const n = Number(index.value); + if (n < 0) { + handleRuntimeError(context, new ValueError(source, command as es.Node, context, "list_ref: negative index")); + } + + let current = list; + for (let i = 0; i < n; i++) { + if (current.type === 'null' || current.type === 'undefined') { + handleRuntimeError(context, new ValueError(source, command as es.Node, context, "list_ref: index out of bounds")); + } + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + current = (current as any).tail; + } + + if (current.type === 'null' || current.type === 'undefined') { + handleRuntimeError(context, new ValueError(source, command as es.Node, context, "list_ref: index out of bounds")); } - const p = args[0]; - if (!p || p.type !== 'pair') { - handleRuntimeError(context, new TypeError(source, command as es.Node, context, p?.type ?? typeof p, 'pair')); + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + return (current as any).head; + } + + @Validate(2, 2, 'member', false) + static member(args: Value[], source: string, command: ControlItem, context: Context): Value { + const value = args[0]; + const list = args[1]; + + let current = list; + while (current.type === 'pair') { + const head = (current as any).head; + + // Check for equality (using === semantics) + if (BuiltInFunctions.valuesEqual(value, head)) { + return current; + } + + current = (current as any).tail; + } + + if (current.type !== 'null' && current.type !== 'undefined') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + return { type: 'null', value: null }; + } + + @Validate(2, 2, 'remove', false) + static remove(args: Value[], source: string, command: ControlItem, context: Context): Value { + const value = args[0]; + const list = args[1]; + + const removeHelper = (current: Value): Value => { + if (current.type === 'null' || current.type === 'undefined') { + return current; + } + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + const head = (current as any).head; + const tail = (current as any).tail; + + if (BuiltInFunctions.valuesEqual(value, head)) { + // Found the value, return the tail (removing this element) + return tail; + } else { + // Keep this element, continue searching in tail + return { + type: 'pair', + head: head, + tail: removeHelper(tail) + }; + } + }; + + return removeHelper(list); + } + + @Validate(2, 2, 'remove_all', false) + static remove_all(args: Value[], source: string, command: ControlItem, context: Context): Value { + const value = args[0]; + const list = args[1]; + + const removeAllHelper = (current: Value): Value => { + if (current.type === 'null' || current.type === 'undefined') { + return current; + } + + if (current.type !== 'pair') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + const head = (current as any).head; + const tail = (current as any).tail; + + const filteredTail = removeAllHelper(tail); + + if (BuiltInFunctions.valuesEqual(value, head)) { + // Remove this element + return filteredTail; + } else { + // Keep this element + return { + type: 'pair', + head: head, + tail: filteredTail + }; + } + }; + + return removeAllHelper(list); + } + + @Validate(2, 2, 'equal', false) + static equal(args: Value[], source: string, command: ControlItem, context: Context): Value { + const val1 = args[0]; + const val2 = args[1]; + + const result = BuiltInFunctions.deepEqual(val1, val2); + return { type: 'bool', value: result }; + } + + @Validate(2, 2, 'build_list', false) + static build_list(args: Value[], source: string, command: ControlItem, context: Context): Value { + const func = args[0]; + const n = args[1]; + + if (func.type !== 'closure') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, func.type, "function")); + } + + if (n.type !== 'bigint') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, n.type, "int")); + } + + const count = Number(n.value); + if (count < 0) { + handleRuntimeError(context, new ValueError(source, command as es.Node, context, "build_list: negative count")); + } + + let result: Value = { type: 'null', value: null }; + + // Build list from right to left (n-1 down to 0) + for (let i = count - 1; i >= 0; i--) { + const indexValue = { type: 'bigint', value: BigInt(i) }; + const element = (func as any).apply(context, [indexValue]); + + result = { + type: 'pair', + head: element, + tail: result + }; + } + + return result; + } + + @Validate(2, 2, 'for_each', false) + static for_each(args: Value[], source: string, command: ControlItem, context: Context): Value { + const func = args[0]; + const list = args[1]; + + if (func.type !== 'closure') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, func.type, "function")); + } + + let current = list; + while (current.type === 'pair') { + const head = (current as any).head; + + // Apply function to head (for side effects) + (func as any).apply(context, [head]); + + current = (current as any).tail; + } + + if (current.type !== 'null' && current.type !== 'undefined') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, "argument", "proper list")); + } + + return { type: 'bool', value: true }; + } + + @Validate(2, 2, 'enum_list', false) + static enum_list(args: Value[], source: string, command: ControlItem, context: Context): Value { + const start = args[0]; + const end = args[1]; + + if (start.type !== 'bigint') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, start.type, "int")); + } + + if (end.type !== 'bigint') { + handleRuntimeError(context, new TypeError(source, command as es.Node, context, end.type, "int")); + } + + const startNum = Number(start.value); + const endNum = Number(end.value); + + let result: Value = { type: 'null', value: null }; + + // Build list from right to left (end down to start) + for (let i = endNum; i >= startNum; i--) { + const element = { type: 'bigint', value: BigInt(i) }; + + result = { + type: 'pair', + head: element, + tail: result + }; + } + + return result; + } + + // Helper function to check if two values are equal (for === semantics) + private static valuesEqual(val1: Value, val2: Value): boolean { + if (val1.type !== val2.type) { + return false; + } + + switch (val1.type) { + case 'null': + case 'undefined': + return true; + case 'bool': + case 'string': + case 'number': + case 'bigint': + return val1.value === val2.value; + case 'complex': + return val1.value.real === val2.value.real && val1.value.imag === val2.value.imag; + default: + return val1 === val2; // Reference equality for functions, etc. + } + } + + // Helper function for deep structural equality (for equal function) + private static deepEqual(val1: Value, val2: Value): boolean { + if (val1.type !== val2.type) { + return false; + } + + switch (val1.type) { + case 'pair': + const head1 = (val1 as any).head; + const tail1 = (val1 as any).tail; + const head2 = (val2 as any).head; + const tail2 = (val2 as any).tail; + + return BuiltInFunctions.deepEqual(head1, head2) && BuiltInFunctions.deepEqual(tail1, tail2); + + case 'null': + case 'undefined': + return true; + + case 'bool': + case 'string': + case 'number': + case 'bigint': + return val1.value === val2.value; + + case 'complex': + return val1.value.real === val2.value.real && val1.value.imag === val2.value.imag; + + default: + return val1 === val2; // Reference equality for functions, etc. } - return p.value.tail; } } import py_s1_constants from './stdlib/py_s1_constants.json'; +import py_s2_constants from './stdlib/py_s2_constants.json'; -// NOTE: If we ever switch to another Python "chapter" (e.g. py_s2_constants), +// NOTE: If we ever switch to another Python “chapter” (e.g. py_s2_constants), // just change the variable below to switch to the set. -const constants = py_s1_constants; +const constants = py_s2_constants; /* Create a map to hold built-in constants. @@ -1623,7 +2105,7 @@ for (const name of constants.builtInFuncs as string[]) { * https://github.com/python/cpython/blob/main/Python/pystrtod.c * * Special cases such as -0, Infinity, and NaN are also handled to ensure that - * output matches Python's display conventions. + * output matches Python’s display conventions. */ export function toPythonFloat(num: number): string { if (Object.is(num, -0)) { diff --git a/src/stdlib/index.ts b/src/stdlib/index.ts new file mode 100644 index 0000000..e6e5d92 --- /dev/null +++ b/src/stdlib/index.ts @@ -0,0 +1,179 @@ +import { BuiltInFunctions, builtInConstants, builtIns } from '../stdlib'; +import { Value } from '../cse-machine/stash'; +import { ControlItem } from '../cse-machine/control'; +import { Context } from '../cse-machine/context'; + +// Chapter 1: Basic Python functions (SOURCE_1 equivalent) +export const chapter_1 = { + // Basic functions + _int: BuiltInFunctions._int, + _int_from_string: BuiltInFunctions._int_from_string, + abs: BuiltInFunctions.abs, + error: BuiltInFunctions.error, + isinstance: BuiltInFunctions.isinstance, + str: BuiltInFunctions.str, + print: BuiltInFunctions.print, + input: BuiltInFunctions.input, + + // Math functions + math_acos: BuiltInFunctions.math_acos, + math_acosh: BuiltInFunctions.math_acosh, + math_asin: BuiltInFunctions.math_asin, + math_asinh: BuiltInFunctions.math_asinh, + math_atan: BuiltInFunctions.math_atan, + math_atan2: BuiltInFunctions.math_atan2, + math_atanh: BuiltInFunctions.math_atanh, + math_cos: BuiltInFunctions.math_cos, + math_cosh: BuiltInFunctions.math_cosh, + math_degrees: BuiltInFunctions.math_degrees, + math_erf: BuiltInFunctions.math_erf, + math_erfc: BuiltInFunctions.math_erfc, + char_at: BuiltInFunctions.char_at, + math_comb: BuiltInFunctions.math_comb, + math_factorial: BuiltInFunctions.math_factorial, + math_gcd: BuiltInFunctions.math_gcd, + math_isqrt: BuiltInFunctions.math_isqrt, + math_lcm: BuiltInFunctions.math_lcm, + math_perm: BuiltInFunctions.math_perm, + math_ceil: BuiltInFunctions.math_ceil, + math_fabs: BuiltInFunctions.math_fabs, + math_floor: BuiltInFunctions.math_floor, + math_fma: BuiltInFunctions.math_fma, + math_fmod: BuiltInFunctions.math_fmod, + math_remainder: BuiltInFunctions.math_remainder, + math_trunc: BuiltInFunctions.math_trunc, + math_copysign: BuiltInFunctions.math_copysign, + math_isfinite: BuiltInFunctions.math_isfinite, + math_isinf: BuiltInFunctions.math_isinf, + math_isnan: BuiltInFunctions.math_isnan, + math_ldexp: BuiltInFunctions.math_ldexp, + math_nextafter: BuiltInFunctions.math_nextafter, + math_ulp: BuiltInFunctions.math_ulp, + math_cbrt: BuiltInFunctions.math_cbrt, + math_exp: BuiltInFunctions.math_exp, + math_exp2: BuiltInFunctions.math_exp2, + math_expm1: BuiltInFunctions.math_expm1, + math_gamma: BuiltInFunctions.math_gamma, + math_lgamma: BuiltInFunctions.math_lgamma, + math_log: BuiltInFunctions.math_log, + math_log10: BuiltInFunctions.math_log10, + math_log1p: BuiltInFunctions.math_log1p, + math_log2: BuiltInFunctions.math_log2, + math_pow: BuiltInFunctions.math_pow, + math_radians: BuiltInFunctions.math_radians, + math_sin: BuiltInFunctions.math_sin, + math_sinh: BuiltInFunctions.math_sinh, + math_tan: BuiltInFunctions.math_tanh, + math_tanh: BuiltInFunctions.math_tanh, + math_sqrt: BuiltInFunctions.math_sqrt, + max: BuiltInFunctions.max, + min: BuiltInFunctions.min, + random_random: BuiltInFunctions.random_random, + round: BuiltInFunctions.round, + time_time: BuiltInFunctions.time_time, +}; + +// Chapter 2: List functions (SOURCE_2 equivalent) +export const chapter_2 = { + ...chapter_1, // Inherit all chapter 1 functions + pair: BuiltInFunctions.pair, + head: BuiltInFunctions.head, + tail: BuiltInFunctions.tail, + is_null: BuiltInFunctions.is_null, + is_pair: BuiltInFunctions.is_pair, + list: BuiltInFunctions.list, + length: BuiltInFunctions.length, + map: BuiltInFunctions.map, + filter: BuiltInFunctions.filter, + accumulate: BuiltInFunctions.accumulate, + append: BuiltInFunctions.append, + reverse: BuiltInFunctions.reverse, + list_ref: BuiltInFunctions.list_ref, + member: BuiltInFunctions.member, + remove: BuiltInFunctions.remove, + remove_all: BuiltInFunctions.remove_all, + equal: BuiltInFunctions.equal, + build_list: BuiltInFunctions.build_list, + for_each: BuiltInFunctions.for_each, + enum_list: BuiltInFunctions.enum_list, +}; + +// Chapter 3: Advanced features (SOURCE_3 equivalent) +export const chapter_3 = { + ...chapter_2, // Inherit all chapter 2 functions + // Add more advanced functions here as they're implemented +}; + +// Chapter 4: Full Python features (SOURCE_4 equivalent) +export const chapter_4 = { + ...chapter_3, // Inherit all chapter 3 functions + // Add full Python features here +}; + +// Library parser chapter +export const library_parser = { + ...chapter_4, // Inherit all chapter 4 functions + // Add library-specific functions here +}; + +// Type definitions for chapters +export type Chapter = 1 | 2 | 3 | 4 | 'LIBRARY_PARSER'; + +// Function to get chapter functions +export function getChapterFunctions(chapter: Chapter) { + switch (chapter) { + case 1: + return chapter_1; + case 2: + return chapter_2; + case 3: + return chapter_3; + case 4: + return chapter_4; + case 'LIBRARY_PARSER': + return library_parser; + default: + throw new Error(`Unknown chapter: ${chapter}`); + } +} + +// Function to get chapter constants +export function getChapterConstants(chapter: Chapter) { + // All chapters have the same constants for now + return builtInConstants; +} + +// Helper function to define builtin in context +export function defineBuiltin( + context: Context, + name: string, + func: (...args: any[]) => any, + arity?: number +) { + const closure = { + type: 'closure' as const, + node: null, + environment: null, + predefined: true, + apply: (ctx: Context, args: Value[]) => { + return func(args, '', {} as ControlItem, ctx); + } + }; + + context.nativeStorage.builtins.set(name, closure); +} + +// Function to import builtins for a specific chapter +export function importBuiltins(context: Context, chapter: Chapter) { + const functions = getChapterFunctions(chapter); + + for (const [name, func] of Object.entries(functions)) { + defineBuiltin(context, name, func); + } + + // Import constants + const constants = getChapterConstants(chapter); + for (const [name, value] of constants) { + context.nativeStorage.builtins.set(name, value); + } +} \ No newline at end of file diff --git a/src/stdlib/preludes.ts b/src/stdlib/preludes.ts new file mode 100644 index 0000000..671c4c5 --- /dev/null +++ b/src/stdlib/preludes.ts @@ -0,0 +1,26 @@ +// Prelude system for py-slang +// Provides Python-language implementations of complex functions + +import { Context } from "../cse-machine/context"; +import { listPrelude } from "../conductor/stdlib/list/list.prelude"; + + +// Local import prelude - for import/export functionality +export const localImportPrelude = ` +# Import/export functionality will be added here when implemented +`; + + + +// Function to import preludes based on chapter +export function importPrelude(context: Context, chapter: number): string { + let prelude = ''; + + if (chapter >= 2) { + prelude += listPrelude; // Adds list functions like map, filter + prelude += localImportPrelude; + } + + + return prelude; +} \ No newline at end of file diff --git a/src/stdlib/py_s1_constants.json b/src/stdlib/py_s1_constants.json index 77b541a..5bd9ac9 100644 --- a/src/stdlib/py_s1_constants.json +++ b/src/stdlib/py_s1_constants.json @@ -62,11 +62,7 @@ "time_time", "str", "print", - "input", - "pair", - "is_pair", - "head", - "tail" + "input" ], "constants": [ "math_e", diff --git a/src/stdlib/py_s2_constants.json b/src/stdlib/py_s2_constants.json new file mode 100644 index 0000000..08c610e --- /dev/null +++ b/src/stdlib/py_s2_constants.json @@ -0,0 +1,94 @@ +{ + "builtInFuncs": [ + "_int", + "_int_from_string", + "abs", + "error", + "isinstance", + "math_acos", + "math_acosh", + "math_asin", + "math_asinh", + "math_atan", + "math_atan2", + "math_atanh", + "math_cos", + "math_cosh", + "math_degrees", + "math_erf", + "math_erfc", + "char_at", + "math_comb", + "math_factorial", + "math_gcd", + "math_isqrt", + "math_lcm", + "math_perm", + "math_ceil", + "math_fabs", + "math_floor", + "math_fma", + "math_fmod", + "math_remainder", + "math_trunc", + "math_copysign", + "math_isfinite", + "math_isinf", + "math_isnan", + "math_ldexp", + "math_nextafter", + "math_ulp", + "math_cbrt", + "math_exp", + "math_exp2", + "math_expm1", + "math_gamma", + "math_lgamma", + "math_log", + "math_log10", + "math_log1p", + "math_log2", + "math_pow", + "math_radians", + "math_sin", + "math_sinh", + "math_tan", + "math_tanh", + "math_sqrt", + "max", + "min", + "random_random", + "round", + "time_time", + "str", + "print", + "input", + "pair", + "head", + "tail", + "is_null", + "is_pair", + "list", + "length", + "map", + "filter", + "accumulate", + "append", + "reverse", + "list_ref", + "member", + "remove", + "remove_all", + "equal", + "build_list", + "for_each", + "enum_list" + ], + "constants": [ + "math_e", + "math_inf", + "math_nan", + "math_pi", + "math_tau" + ] +} \ No newline at end of file diff --git a/src/tests/prelude.test.ts b/src/tests/prelude.test.ts new file mode 100644 index 0000000..a5eeaef --- /dev/null +++ b/src/tests/prelude.test.ts @@ -0,0 +1,292 @@ +import { createContext, Chapter } from '../createContext'; +import { runInContext } from '../index'; +import { Context } from '../cse-machine/context'; + +describe('Prelude System Tests', () => { + describe('Context Creation', () => { + test('should create context with Chapter 1', () => { + const context = createContext(1); + expect(context).toBeInstanceOf(Context); + expect(context.prelude).toBeNull(); // Chapter 1 has no prelude + }); + + test('should create context with Chapter 2', () => { + const context = createContext(2); + expect(context).toBeInstanceOf(Context); + expect(context.prelude).toBeTruthy(); // Chapter 2 should have prelude + }); + + test('should create context with Chapter 3', () => { + const context = createContext(3); + expect(context).toBeInstanceOf(Context); + expect(context.prelude).toBeTruthy(); + }); + + test('should create context with Chapter 4', () => { + const context = createContext(4); + expect(context).toBeInstanceOf(Context); + expect(context.prelude).toBeTruthy(); + }); + + test('should create context with LIBRARY_PARSER', () => { + const context = createContext('LIBRARY_PARSER'); + expect(context).toBeInstanceOf(Context); + expect(context.prelude).toBeTruthy(); + }); + }); + + describe('Chapter 1 Functions', () => { + let context: Context; + + beforeEach(() => { + context = createContext(1); + }); + + test('should have basic math functions', async () => { + const code = ` +print(abs(-5)) +print(max(1, 2, 3)) +print(min(1, 2, 3)) +print(round(3.14159, 2)) +print(str(42)) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have math constants', async () => { + const code = ` +print(math_pi) +print(math_e) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + }); + + describe('Chapter 2 List Functions', () => { + let context: Context; + + beforeEach(() => { + context = createContext(2); + }); + + test('should have list creation functions', async () => { + const code = ` +xs = list(1, 2, 3, 4, 5) +print(xs) +print(length(xs)) +print(head(xs)) +print(tail(xs)) +print(is_pair(xs)) +print(is_null(None)) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have map function', async () => { + const code = ` +def square(x): + return x * x + +xs = list(1, 2, 3, 4, 5) +squared = map(square, xs) +print(squared) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have filter function', async () => { + const code = ` +def is_even(x): + return x % 2 == 0 + +xs = list(1, 2, 3, 4, 5) +evens = filter(is_even, xs) +print(evens) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have accumulate function', async () => { + const code = ` +def add(x, y): + return x + y + +xs = list(1, 2, 3, 4, 5) +sum_xs = accumulate(add, 0, xs) +print(sum_xs) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have reverse function', async () => { + const code = ` +xs = list(1, 2, 3, 4, 5) +reversed_xs = reverse(xs) +print(reversed_xs) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have append function', async () => { + const code = ` +xs = list(1, 2, 3) +ys = list(4, 5, 6) +appended = append(xs, ys) +print(appended) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have build_list function', async () => { + const code = ` +def double(x): + return x * 2 + +doubled_list = build_list(double, 5) +print(doubled_list) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have enum_list function', async () => { + const code = ` +range_list = enum_list(1, 10) +print(range_list) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have list utility functions', async () => { + const code = ` +xs = list(1, 2, 3, 4, 5) +print(list_ref(xs, 2)) +print(member(3, xs)) +print(member(10, xs)) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have remove functions', async () => { + const code = ` +xs = list(1, 2, 3, 2, 4, 2, 5) +removed_first = remove(2, xs) +removed_all = remove_all(2, xs) +print(removed_first) +print(removed_all) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have equal function', async () => { + const code = ` +list1 = list(1, 2, 3) +list2 = list(1, 2, 3) +list3 = list(1, 2, 4) +print(equal(list1, list2)) +print(equal(list1, list3)) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should have for_each function', async () => { + const code = ` +def print_item(x): + print("Processing:", x) + +xs = list(1, 2, 3) +for_each(print_item, xs) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + }); + + describe('Prelude Loading', () => { + test('should load prelude for Chapter 2+', () => { + const context = createContext(2); + expect(context.prelude).toBeTruthy(); + expect(typeof context.prelude).toBe('string'); + expect(context.prelude!.length).toBeGreaterThan(0); + }); + + test('should not load prelude for Chapter 1', () => { + const context = createContext(1); + expect(context.prelude).toBeNull(); + }); + }); + + describe('Integration Tests', () => { + test('should run complex list operations', async () => { + const context = createContext(2); + const code = ` +# Create a list +xs = list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) + +# Filter even numbers +def is_even(x): + return x % 2 == 0 + +evens = filter(is_even, xs) + +# Map square function +def square(x): + return x * x + +squared_evens = map(square, evens) + +# Accumulate sum +def add(x, y): + return x + y + +sum_squared_evens = accumulate(add, 0, squared_evens) + +print("Original list:", xs) +print("Even numbers:", evens) +print("Squared evens:", squared_evens) +print("Sum of squared evens:", sum_squared_evens) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + + test('should handle nested list operations', async () => { + const context = createContext(2); + const code = ` +# Create lists +list1 = list(1, 2, 3) +list2 = list(4, 5, 6) +list3 = list(7, 8, 9) + +# Append lists +combined = append(list1, append(list2, list3)) + +# Reverse the combined list +reversed_combined = reverse(combined) + +# Get specific elements +first = list_ref(combined, 0) +last = list_ref(combined, 8) + +print("Combined list:", combined) +print("Reversed list:", reversed_combined) +print("First element:", first) +print("Last element:", last) +`; + const result = await runInContext(code, context); + expect(result).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/src/tests/stdlib.test.ts b/src/tests/stdlib.test.ts new file mode 100644 index 0000000..18e100e --- /dev/null +++ b/src/tests/stdlib.test.ts @@ -0,0 +1,185 @@ +import { createContext, Chapter } from '../createContext'; +import { importBuiltins } from '../stdlib/index'; +import { Context } from '../cse-machine/context'; + +describe('Standard Library Tests', () => { + describe('Chapter 1 Builtins', () => { + let context: Context; + + beforeEach(() => { + context = new Context(); + importBuiltins(context, 1); + }); + + test('should have basic math functions', () => { + expect(context.nativeStorage.builtins.has('abs')).toBe(true); + expect(context.nativeStorage.builtins.has('max')).toBe(true); + expect(context.nativeStorage.builtins.has('min')).toBe(true); + expect(context.nativeStorage.builtins.has('round')).toBe(true); + expect(context.nativeStorage.builtins.has('str')).toBe(true); + expect(context.nativeStorage.builtins.has('print')).toBe(true); + expect(context.nativeStorage.builtins.has('input')).toBe(true); + }); + + test('should have math constants', () => { + expect(context.nativeStorage.builtins.has('math_pi')).toBe(true); + expect(context.nativeStorage.builtins.has('math_e')).toBe(true); + expect(context.nativeStorage.builtins.has('math_inf')).toBe(true); + expect(context.nativeStorage.builtins.has('math_nan')).toBe(true); + expect(context.nativeStorage.builtins.has('math_tau')).toBe(true); + }); + + test('should have type checking functions', () => { + expect(context.nativeStorage.builtins.has('isinstance')).toBe(true); + expect(context.nativeStorage.builtins.has('error')).toBe(true); + }); + + test('should have math functions', () => { + expect(context.nativeStorage.builtins.has('math_sin')).toBe(true); + expect(context.nativeStorage.builtins.has('math_cos')).toBe(true); + expect(context.nativeStorage.builtins.has('math_sqrt')).toBe(true); + expect(context.nativeStorage.builtins.has('math_pow')).toBe(true); + expect(context.nativeStorage.builtins.has('math_log')).toBe(true); + }); + }); + + describe('Chapter 2 Builtins', () => { + let context: Context; + + beforeEach(() => { + context = new Context(); + importBuiltins(context, 2); + }); + + test('should have all Chapter 1 functions', () => { + expect(context.nativeStorage.builtins.has('abs')).toBe(true); + expect(context.nativeStorage.builtins.has('math_pi')).toBe(true); + expect(context.nativeStorage.builtins.has('print')).toBe(true); + }); + + test('should have list creation functions', () => { + expect(context.nativeStorage.builtins.has('pair')).toBe(true); + expect(context.nativeStorage.builtins.has('head')).toBe(true); + expect(context.nativeStorage.builtins.has('tail')).toBe(true); + expect(context.nativeStorage.builtins.has('is_null')).toBe(true); + expect(context.nativeStorage.builtins.has('is_pair')).toBe(true); + expect(context.nativeStorage.builtins.has('list')).toBe(true); + }); + + test('should have list operation functions', () => { + expect(context.nativeStorage.builtins.has('length')).toBe(true); + expect(context.nativeStorage.builtins.has('map')).toBe(true); + expect(context.nativeStorage.builtins.has('filter')).toBe(true); + expect(context.nativeStorage.builtins.has('accumulate')).toBe(true); + expect(context.nativeStorage.builtins.has('append')).toBe(true); + expect(context.nativeStorage.builtins.has('reverse')).toBe(true); + }); + + test('should have list utility functions', () => { + expect(context.nativeStorage.builtins.has('list_ref')).toBe(true); + expect(context.nativeStorage.builtins.has('member')).toBe(true); + expect(context.nativeStorage.builtins.has('remove')).toBe(true); + expect(context.nativeStorage.builtins.has('remove_all')).toBe(true); + expect(context.nativeStorage.builtins.has('equal')).toBe(true); + }); + + test('should have list generation functions', () => { + expect(context.nativeStorage.builtins.has('build_list')).toBe(true); + expect(context.nativeStorage.builtins.has('for_each')).toBe(true); + expect(context.nativeStorage.builtins.has('enum_list')).toBe(true); + }); + }); + + describe('Chapter 3 Builtins', () => { + let context: Context; + + beforeEach(() => { + context = new Context(); + importBuiltins(context, 3); + }); + + test('should have all Chapter 2 functions', () => { + expect(context.nativeStorage.builtins.has('abs')).toBe(true); + expect(context.nativeStorage.builtins.has('pair')).toBe(true); + expect(context.nativeStorage.builtins.has('map')).toBe(true); + }); + + test('should have Chapter 3 specific functions', () => { + // Chapter 3 functions would be added here when implemented + // For now, it should have all Chapter 2 functions + expect(context.nativeStorage.builtins.has('length')).toBe(true); + expect(context.nativeStorage.builtins.has('filter')).toBe(true); + }); + }); + + describe('Chapter 4 Builtins', () => { + let context: Context; + + beforeEach(() => { + context = new Context(); + importBuiltins(context, 4); + }); + + test('should have all Chapter 3 functions', () => { + expect(context.nativeStorage.builtins.has('abs')).toBe(true); + expect(context.nativeStorage.builtins.has('pair')).toBe(true); + expect(context.nativeStorage.builtins.has('map')).toBe(true); + }); + + test('should have Chapter 4 specific functions', () => { + // Chapter 4 functions would be added here when implemented + // For now, it should have all Chapter 3 functions + expect(context.nativeStorage.builtins.has('length')).toBe(true); + expect(context.nativeStorage.builtins.has('filter')).toBe(true); + }); + }); + + describe('LIBRARY_PARSER Builtins', () => { + let context: Context; + + beforeEach(() => { + context = new Context(); + importBuiltins(context, 'LIBRARY_PARSER'); + }); + + test('should have all Chapter 4 functions', () => { + expect(context.nativeStorage.builtins.has('abs')).toBe(true); + expect(context.nativeStorage.builtins.has('pair')).toBe(true); + expect(context.nativeStorage.builtins.has('map')).toBe(true); + }); + + test('should have library parser specific functions', () => { + // Library parser functions would be added here when implemented + // For now, it should have all Chapter 4 functions + expect(context.nativeStorage.builtins.has('length')).toBe(true); + expect(context.nativeStorage.builtins.has('filter')).toBe(true); + }); + }); + + describe('Function Values', () => { + test('should have callable functions', () => { + const context = new Context(); + importBuiltins(context, 2); + + const absFunc = context.nativeStorage.builtins.get('abs'); + const mapFunc = context.nativeStorage.builtins.get('map'); + const listFunc = context.nativeStorage.builtins.get('list'); + + expect(typeof absFunc).toBe('function'); + expect(typeof mapFunc).toBe('function'); + expect(typeof listFunc).toBe('function'); + }); + + test('should have correct function signatures', () => { + const context = new Context(); + importBuiltins(context, 2); + + // Test that functions can be called (basic functionality) + const absFunc = context.nativeStorage.builtins.get('abs'); + const listFunc = context.nativeStorage.builtins.get('list'); + + expect(absFunc).toBeDefined(); + expect(listFunc).toBeDefined(); + }); + }); +}); \ No newline at end of file