From ccf1a8bc238f685bce5906ab285652fc32bb2b37 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Mon, 23 Mar 2026 22:06:30 +0800 Subject: [PATCH 01/19] Add basic syntax highlighting and autocomplete --- .gitignore | 3 + eslint.config.mjs | 2 +- package.json | 9 +- scripts/autocomplete.sh | 21 ++ src/conductor/PyEvaluator.ts | 2 + .../plugins/autocomplete/constants.ts | 5 + .../plugins/autocomplete/highlight-rules.ts | 229 ++++++++++++++++++ src/conductor/plugins/autocomplete/index.ts | 55 +++++ src/conductor/plugins/autocomplete/mode.ts | 24 ++ .../plugins/autocomplete/resolver.ts | 162 +++++++++++++ src/conductor/plugins/autocomplete/types.ts | 99 ++++++++ src/generate-autocomplete.mts | 106 ++++++++ yarn.lock | 47 +++- 13 files changed, 754 insertions(+), 10 deletions(-) create mode 100755 scripts/autocomplete.sh create mode 100644 src/conductor/plugins/autocomplete/constants.ts create mode 100644 src/conductor/plugins/autocomplete/highlight-rules.ts create mode 100644 src/conductor/plugins/autocomplete/index.ts create mode 100644 src/conductor/plugins/autocomplete/mode.ts create mode 100644 src/conductor/plugins/autocomplete/resolver.ts create mode 100644 src/conductor/plugins/autocomplete/types.ts create mode 100644 src/generate-autocomplete.mts diff --git a/.gitignore b/.gitignore index aa2e5b0f..05f7399e 100644 --- a/.gitignore +++ b/.gitignore @@ -117,3 +117,6 @@ test.py .yarn/* .pnp.cjs .pnp.loader.mjs + +# Autocomplete generated files +src/conductor/plugins/autocomplete/builtins/*.json diff --git a/eslint.config.mjs b/eslint.config.mjs index f11e9a74..4c4a03ff 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -17,7 +17,7 @@ export default defineConfig([ tseslint.configs.recommended, eslintConfigPrettierFlat, { - files: ["src/**/*.{ts,tsx}"], + files: ["src/**/*.{ts,tsx,mts}"], languageOptions: { parser: tseslint.parser, parserOptions: { diff --git a/package.json b/package.json index 44f942b7..20c475e6 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "regen": "npx tsx src/generate.ts && prettier --write src/ast-types.ts", "compile-grammar": "nearleyc src/parser/python.ne -o src/parser/python-grammar.ts && prettier --write src/parser/python-grammar.ts", "start:dev": "npx nodemon", - "build": "rollup -c --bundleConfigAsCjs", + "build": "./scripts/autocomplete.sh && rollup -c --bundleConfigAsCjs", "start": "yarn run build && node dist/index.js", "jsdoc": "./scripts/jsdoc.sh prepare", "jsdoc:run": "./scripts/jsdoc.sh run", @@ -16,8 +16,8 @@ "test": "jest", "test-coverage": "jest --coverage", "lint": "eslint --concurrency=auto src", - "format": "prettier --write \"**/*.{ts,tsx,json,js,mjs}\"", - "format:ci": "prettier --list-different \"**/*.{ts,tsx,json,js,mjs}\"", + "format": "prettier --write \"**/*.{ts,tsx,json,js,mjs,mts}\"", + "format:ci": "prettier --list-different \"**/*.{ts,tsx,json,js,mjs,mts}\"", "wasm": "ts-node src/wasm-compiler/index.ts" }, "keywords": [ @@ -32,6 +32,7 @@ "license": "Apache-2.0", "packageManager": "yarn@4.13.0+sha512.5c20ba010c99815433e5c8453112165e673f1c7948d8d2b267f4b5e52097538658388ebc9f9580656d9b75c5cc996f990f611f99304a2197d4c56d21eea370e7", "devDependencies": { + "@lezer/python": "^1.1.18", "@rollup/plugin-commonjs": "^28.0.3", "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", @@ -55,7 +56,7 @@ "typescript-eslint": "^8.56.1" }, "dependencies": { - "@sourceacademy/conductor": "^0.2.3", + "@sourceacademy/conductor": "^0.3.0", "@sourceacademy/wasm-util": "^1.0.4", "fast-levenshtein": "^3.0.0", "mathjs": "^14.9.1", diff --git a/scripts/autocomplete.sh b/scripts/autocomplete.sh new file mode 100755 index 00000000..e883abfc --- /dev/null +++ b/scripts/autocomplete.sh @@ -0,0 +1,21 @@ +#! /usr/bin/env bash + +# Exit immediately if a command exits with a non-zero status. +set -e + +JSDOC="$(yarn bin jsdoc)" +LIB="docs/lib" +CONF="docs/jsdoc/conf.json" + +# Create the builtins directory if it doesn't exist +mkdir -p "src/conductor/plugins/autocomplete/builtins" + +# Process every JavaScript file in the docs/lib directory with JSDoc, +# outputting the AST as JSON to the src/conductor/plugins/autocomplete/builtins directory. +for file in "$LIB"/*.js; do + echo "Processing $file..." + $JSDOC -X -c "$CONF" "$file" > "src/conductor/plugins/autocomplete/builtins/$(basename "$file" .js).json" & +done +wait + +yarn ts-node src/generate-autocomplete.mts diff --git a/src/conductor/PyEvaluator.ts b/src/conductor/PyEvaluator.ts index 49e96415..4d30168b 100644 --- a/src/conductor/PyEvaluator.ts +++ b/src/conductor/PyEvaluator.ts @@ -6,6 +6,7 @@ import { BasicEvaluator, IRunnerPlugin } from "@sourceacademy/conductor/runner"; import { Context } from "../cse-machine/context"; import { IOptions, runInContext } from "../runner/pyRunner"; import { Finished } from "../types"; +import AutoCompletePlugin from "./plugins/autocomplete"; const defaultContext = new Context(); const defaultOptions: IOptions = { @@ -22,6 +23,7 @@ export default class PyEvaluator extends BasicEvaluator { super(conductor); this.context = defaultContext; this.options = defaultOptions; + conductor.registerPlugin(AutoCompletePlugin, 1); } async evaluateChunk(chunk: string): Promise { diff --git a/src/conductor/plugins/autocomplete/constants.ts b/src/conductor/plugins/autocomplete/constants.ts new file mode 100644 index 00000000..bc0faa28 --- /dev/null +++ b/src/conductor/plugins/autocomplete/constants.ts @@ -0,0 +1,5 @@ +export const WEB_PLUGIN_ID = "__autocomplete_plugin_web"; +export const RUNNER_PLUGIN_ID = "__autocomplete_plugin_runner"; + +export const AUTOCOMPLETE_CHANNEL_ID = "__autocomplete_channel__"; +export const SYNTAX_CHANNEL_ID = "__syntax_channel__"; diff --git a/src/conductor/plugins/autocomplete/highlight-rules.ts b/src/conductor/plugins/autocomplete/highlight-rules.ts new file mode 100644 index 00000000..d8ac65fa --- /dev/null +++ b/src/conductor/plugins/autocomplete/highlight-rules.ts @@ -0,0 +1,229 @@ +/** + * Adapted from https://github.com/ajaxorg/ace/blob/master/src/mode/python_highlight_rules.js + * + * Copyright (c) 2010, Ajax.org B.V. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of Ajax.org B.V. nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL AJAX.ORG B.V. BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + */ + +import constants from "../../../stdlib/py_s1_constants.json"; +import { AceRules } from "./types"; +export default (variant: number) => { + let keywords = + "and|def|elif|else|from|global|if|import|lambda|not|or|pass|return|nonlocal"; + + let illegalKeywords = + "as|assert|class|del|except|finally|is|match|case|raise|try|with|yield|async|await"; + + if (variant < 3) { + illegalKeywords += "|while|for|break|continue|is"; + } else { + keywords += "|while|for|break|continue"; + } + + + const builtinConstants = constants.constants.join("|"); + + const builtinFunctions = constants.builtInFuncs.join("|"); + + //var futureReserved = ""; + const keywordMapper = { + map: { + "invalid.deprecated": "debugger", + "support.function": builtinFunctions, + "variable.language": "self|cls", + "constant.language": builtinConstants, + keyword: keywords, + "invalid.illegal": illegalKeywords, + }, + defaultToken: "identifier", + }; + + const decimalInteger = "(?:(?:[1-9]\\d*)|(?:0))"; + const octInteger = "(?:0[oO]?[0-7]+)"; + const hexInteger = "(?:0[xX][\\dA-Fa-f]+)"; + const binInteger = "(?:0[bB][01]+)"; + const integer = + "(?:" + decimalInteger + "|" + octInteger + "|" + hexInteger + "|" + binInteger + ")"; + + const exponent = "(?:[eE][+-]?\\d+)"; + const fraction = "(?:\\.\\d+)"; + const intPart = "(?:\\d+)"; + const pointFloat = "(?:(?:" + intPart + "?" + fraction + ")|(?:" + intPart + "\\.))"; + const exponentFloat = "(?:(?:" + pointFloat + "|" + intPart + ")" + exponent + ")"; + const floatNumber = "(?:" + exponentFloat + "|" + pointFloat + ")"; + + const stringEscape = + "\\\\(x[0-9A-Fa-f]{2}|[0-7]{3}|[\\\\abfnrtv'\"]|U[0-9A-Fa-f]{8}|u[0-9A-Fa-f]{4})"; + const rules: AceRules = { + start: [ + { + token: "comment", + regex: "#.*$", + }, + { + token: "string", // multi line """ string start + regex: '"{3}', + next: "qqstring3", + }, + { + token: "string", // " string + regex: '"(?=.)', + next: "qqstring", + }, + { + token: "string", // multi line ''' string start + regex: "'{3}", + next: "qstring3", + }, + { + token: "string", // ' string + regex: "'(?=.)", + next: "qstring", + }, + { + token: "keyword.operator", + regex: "\\+|\\-|\\*|\\*\\*|\\/|\\/\\/|%|@|<<|>>|&|\\||\\^|~|<|>|<=|=>|==|!=|<>|=", + }, + { + token: "punctuation", + regex: ",|:|;|\\->|\\+=|\\-=|\\*=|\\/=|\\/\\/=|%=|@=|&=|\\|=|^=|>>=|<<=|\\*\\*=", + }, + { + token: "paren.lparen", + regex: "[\\[\\(\\{]", + }, + { + token: "paren.rparen", + regex: "[\\]\\)\\}]", + }, + { + token: ["keyword", "text", "entity.name.function"], + regex: "(def|class)(\\s+)([\\u00BF-\\u1FFF\\u2C00-\\uD7FF\\w]+)", + }, + { + token: "text", + regex: "\\s+", + }, + { + include: "constants", + }, + ], + qqstring3: [ + { + token: "constant.language.escape", + regex: stringEscape, + }, + { + token: "string", // multi line """ string end + regex: '"{3}', + next: "start", + }, + { + defaultToken: "string", + }, + ], + qstring3: [ + { + token: "constant.language.escape", + regex: stringEscape, + }, + { + token: "string", // multi line ''' string end + regex: "'{3}", + next: "start", + }, + { + defaultToken: "string", + }, + ], + qqstring: [ + { + token: "constant.language.escape", + regex: stringEscape, + }, + { + token: "string", + regex: "\\\\$", + next: "qqstring", + }, + { + token: "string", + regex: '"|$', + next: "start", + }, + { + defaultToken: "string", + }, + ], + qstring: [ + { + token: "constant.language.escape", + regex: stringEscape, + }, + { + token: "string", + regex: "\\\\$", + next: "qstring", + }, + { + token: "string", + regex: "'|$", + next: "start", + }, + { + defaultToken: "string", + }, + ], + constants: [ + { + token: "constant.numeric", // imaginary + regex: "(?:" + floatNumber + "|\\d+)[jJ]\\b", + }, + { + token: "constant.numeric", // float + regex: floatNumber, + }, + { + token: "constant.numeric", // long integer + regex: integer + "[lL]\\b", + }, + { + token: "constant.numeric", // integer + regex: integer + "\\b", + }, + { + token: ["punctuation", "function.support"], // method + regex: "(\\.)([a-zA-Z_]+)\\b", + }, + { + token: keywordMapper, + regex: "[a-zA-Z_$][a-zA-Z0-9_$]*\\b", + }, + ], + }; + + return rules; +}; diff --git a/src/conductor/plugins/autocomplete/index.ts b/src/conductor/plugins/autocomplete/index.ts new file mode 100644 index 00000000..9069f231 --- /dev/null +++ b/src/conductor/plugins/autocomplete/index.ts @@ -0,0 +1,55 @@ +import { parser } from "@lezer/python"; +import { IChannel, IConduit, IPlugin } from "@sourceacademy/conductor/conduit"; + +import { AUTOCOMPLETE_CHANNEL_ID, RUNNER_PLUGIN_ID, SYNTAX_CHANNEL_ID } from "./constants"; +import pythonMode from "./mode"; +import { getNames } from "./resolver"; +import type { AutoCompleteMessage, SyntaxHighlightMessage } from "./types"; + + +/** + * This plugin provides autocomplete suggestions and syntax highlighting for Python code. + * + * It provides two channels: one for autocomplete requests and responses, and another for sending syntax highlighting information to the web plugin. + * a) The autocomplete channel listens for requests containing the current code and cursor position. It uses the resolver to find relevant symbols based on the cursor position and sends back a response with the autocomplete suggestions. + * b) The syntax highlighting channel periodically sends the Python mode information to the web plugin until it receives an acknowledgment, ensuring that the web plugin has the necessary information to perform syntax highlighting. + */ +export default class AutoCompletePlugin implements IPlugin { + static readonly channelAttach = [AUTOCOMPLETE_CHANNEL_ID, SYNTAX_CHANNEL_ID]; + readonly id: string = RUNNER_PLUGIN_ID; + + private readonly __autoCompleteChannel: IChannel; + private readonly __syntaxHighlightChannel: IChannel; + + constructor( + _conduit: IConduit, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + [autoCompleteChannel, syntaxHighlightChannel]: IChannel[], + variant: number, + ) { + this.__autoCompleteChannel = autoCompleteChannel; + this.__syntaxHighlightChannel = syntaxHighlightChannel; + const handler = setInterval(() => { + this.__syntaxHighlightChannel.send({ + type: "message", + data: pythonMode, + }); + }, 1000); + this.__syntaxHighlightChannel.subscribe((message: SyntaxHighlightMessage) => { + if (message.type === "ack") { + clearInterval(handler); + } + }); + this.__autoCompleteChannel.subscribe((message: AutoCompleteMessage) => { + if (message.type === "request") { + const tree = parser.parse(message.code); + + const entries = getNames(tree, message.code, message.row, message.column, variant); + this.__autoCompleteChannel.send({ + type: "response", + declarations: entries, + }); + } + }); + } +} diff --git a/src/conductor/plugins/autocomplete/mode.ts b/src/conductor/plugins/autocomplete/mode.ts new file mode 100644 index 00000000..847984b5 --- /dev/null +++ b/src/conductor/plugins/autocomplete/mode.ts @@ -0,0 +1,24 @@ +import PythonHighlightRules from "./highlight-rules"; +export default { + highlightRules: PythonHighlightRules(1), + foldingRules: { + hookFrom: "ace/mode/folding/pythonic", + args: ["\\:"], + }, + lineCommentStart: "#", + pairQuotesAfter: { + "'": /[ruf]/i, + '"': /[ruf]/i, + }, + indents: { + hookFrom: "ace/mode/python", + }, + outdents: { + hookFrom: "ace/mode/python", + }, + autoOutdent: { + hookFrom: "ace/mode/python", + }, + id: "ace/mode/pythonBetaDefault", + snippetFileId: "ace/snippets/pythonBetaDefault", +}; diff --git a/src/conductor/plugins/autocomplete/resolver.ts b/src/conductor/plugins/autocomplete/resolver.ts new file mode 100644 index 00000000..584823ca --- /dev/null +++ b/src/conductor/plugins/autocomplete/resolver.ts @@ -0,0 +1,162 @@ +import { SyntaxNode, Tree, TreeCursor } from "@lezer/common"; +import { AutoCompleteEntry, CompletionItemKind } from "./types"; + +import mathJSON from "./builtins/math.json"; +import miscJSON from "./builtins/misc.json"; + +type Environment = { + variables: string[]; + functions: string[]; + child: Environment | null; +}; + +const getNodeText = (node: SyntaxNode, doc: string): string => { + return doc.slice(node.from, node.to); +}; + +function isCompletionItemKind(value: string): value is CompletionItemKind { + return Object.values(CompletionItemKind).includes(value as CompletionItemKind); +} +export const extractEnvironment = ( + iter: TreeCursor, + pos: number, + doc: string, +): Environment | null => { + const topEnv: Environment = { + variables: [], + functions: [], + child: null, + }; + + let currentEnv = topEnv; + while (iter) { + if (iter.node.type.name === "ParamList") { + return null; + } + if (iter.node.type.name == "FunctionDefinition" || iter.node.type.name == "LambdaExpression") { + // Add function parameters to inner environment + const params = iter.node.getChild("ParamList"); + if (params) { + for (let param = params.firstChild; param; param = param.nextSibling) { + if (param.type.name === "VariableName") { + currentEnv.variables.push(getNodeText(param, doc)); + } + } + } + if (!iter.enter(pos, -1)) { + break; + } + continue; + } + if (iter.node.type.name !== "Block" && iter.node.type.name !== "Script") { + if (!iter.enter(pos, -1)) { + break; + } + continue; + } + // Iterate children + for (let child = iter.node.firstChild; child; child = child.nextSibling) { + // Assignment → variable + if (child.type.name === "AssignStatement") { + const left = child.firstChild; + if ( + left && + left.type.name === "VariableName" && + (left.from != iter.node.from || left.to != iter.node.to) + ) { + currentEnv.variables.push(getNodeText(left, doc)); + } + } + + // Function + if (child.type.name === "FunctionDefinition") { + const nameNode = child.getChild("VariableName"); + if (nameNode) { + currentEnv.functions.push(getNodeText(nameNode, doc)); + } + } + } + const nextEnv = { + variables: [], + functions: [], + child: null, + }; + currentEnv.child = nextEnv; + currentEnv = nextEnv; + + if (!iter.enter(pos, -1)) { + break; + } + } + return topEnv; +}; + +const isSubsequence = (sub: string, str: string): boolean => { + let i = 0; + for (const char of str) { + if (char === sub[i]) { + i++; + } + if (i === sub.length) { + return true; + } + } + return false; +}; + +const convertPosToIndex = (doc: string, line: number, column: number): number => { + let pos = 0; + while (line > 0) { + const newlineIndex = doc.indexOf("\n", pos); + if (newlineIndex === -1) { + return doc.length; + } + pos = newlineIndex + 1; + line--; + } + return pos + column; +}; + +export const getNames = ( + tree: Tree, + doc: string, + line: number, + column: number, + _variant: number, +): AutoCompleteEntry[] => { + const pos = convertPosToIndex(doc, line - 1, column); + const node = tree.resolve(pos, -1); + if (node.type.name !== "VariableName") { + return []; + } + + const query = getNodeText(node, doc); + + let env: Environment | null = extractEnvironment(tree.cursor(), pos, doc); + const entries: AutoCompleteEntry[] = []; + let score = 1; + while (env) { + const symbols = [ + ...env.variables.map(v => ({ name: v, meta: CompletionItemKind.Variable, score: score })), + ...env.functions.map(f => ({ name: f, meta: CompletionItemKind.Function, score: score })), + ]; + symbols + .filter(s => isSubsequence(query, s.name)) + .sort((a, b) => a.name.localeCompare(b.name)) + .forEach(s => entries.push(s)); + + env = env.child; + score++; + } + const symbols = [...miscJSON, ...mathJSON].map(v => ({ + name: v.name, + meta: isCompletionItemKind(v.meta) ? v.meta : CompletionItemKind.Variable, + docHTML: "

" + v.title + "

" + v.description + "

", + })); + symbols + .filter(s => isSubsequence(query, s.name)) + .sort((a, b) => a.name.localeCompare(b.name)) + .forEach(s => entries.push(s)); + + return entries; +}; diff --git a/src/conductor/plugins/autocomplete/types.ts b/src/conductor/plugins/autocomplete/types.ts new file mode 100644 index 00000000..2da0e82a --- /dev/null +++ b/src/conductor/plugins/autocomplete/types.ts @@ -0,0 +1,99 @@ +export type AutoCompleteRequest = { + type: "request"; + code: string; + row: number; + column: number; +}; +export type AutoCompleteResponse = { + type: "response"; + declarations: AutoCompleteEntry[]; +}; + +export type AutoCompleteMessage = AutoCompleteRequest | AutoCompleteResponse; + +export type SyntaxHighlightData = { + highlightRules: AceRules; + foldingRules: { + hookFrom: string; + args: string[]; + }; + lineCommentStart: string; + pairQuotesAfter: Record; + indents: { + hookFrom: string; + }; + outdents: { + hookFrom: string; + }; + autoOutdent: { + hookFrom: string; + }; + id: string; + snippetFileId: string; +}; + +export type KeywordMapperArgs = { + map: Record; + defaultToken: string; +}; + +export type AceRule = + | { + token: string | string[] | KeywordMapperArgs; + regex: string; + next?: string; + push?: string; + } + | { + include: string; + } + | { + defaultToken: string; + }; + +export type AceRules = { + [state: string]: AceRule[]; +}; + +export type SyntaxHighlightMessage = + | { + type: "message"; + data: SyntaxHighlightData; + } + | { type: "ack" }; + +// Lifted from https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.completionitemkind?view=visualstudiosdk-2022 +export enum CompletionItemKind { + Text = "text", + Method = "method", + Function = "func", + Constructor = "constructor", + Field = "field", + Variable = "var", + Class = "class", + Interface = "interface", + Module = "module", + Property = "property", + Unit = "unit", + Value = "value", + Enum = "enum", + Keyword = "keyword", + Snippet = "snippet", + Color = "color", + File = "file", + Reference = "reference", + Folder = "folder", + EnumMember = "enumMember", + Constant = "constant", + Struct = "struct", + Event = "event", + Operator = "operator", + TypeParameter = "typeParameter", +} + +export interface AutoCompleteEntry { + name: string; + meta: CompletionItemKind; + score?: number; + docHTML?: string; +} diff --git a/src/generate-autocomplete.mts b/src/generate-autocomplete.mts new file mode 100644 index 00000000..a9ec9e32 --- /dev/null +++ b/src/generate-autocomplete.mts @@ -0,0 +1,106 @@ +import { readdir, writeFile } from "node:fs/promises"; + +/** + * This script generates autocomplete data for built-in functions and variables + * by parsing raw JSDoc ASTs in `src/conductor/plugins/autocomplete/builtins`. + * It assumes that the JSDoc ASTs are already generated by running `jsdoc -X` on the built-in JavaScript files. + * + * In most circumstances, you should not need to run this script manually, + * as it is automatically invoked by the `build` script in `package.json`. + * If you want to regenerate the autocomplete data, you can run `./scripts/autocomplete.sh`, + * which will first regenerate the JSDoc ASTs and then run this script to generate the autocomplete data. + * + * It outputs JSON files containing the name, title, description, and meta information for each function and variable, + * which are then used by the autocomplete plugin. + * + * Note: This script may not have all the type information for the built-ins. Feel free to edit the Entry type and the mapping logic to include more information if needed. + */ + +type Entry = + | { + comment: string; + description: string; + params: { + type: { + names: string[]; + }; + description: string; + name: string; + }[]; + returns?: { + type: { + names: string[]; + }; + description: string; + }[]; + name: string; + longname: string; + kind: "function"; + scope: string; + } + | { + kind: "package"; + longname: string; + files: string[]; + } + | { + comment: string; + description: string; + type: { + names: string[]; + }; + name: string; + longname: string; + kind: "constant"; + scope: string; + }; + +/** + * The generation process is as follows: + * 1. Find all the JSON files in the builtins directory, which are generated by running `jsdoc -X` on the built-in JavaScript files. + * 2. For each JSON file, create a promise which + * a. Reads the JSON file and parses it into an array of Entry objects. + * b. Filters the entries to only include functions and constants, and maps them to a simplified format containing the name, title, description, and meta information. + * c. Writes the simplified entries back to the same JSON file, overwriting the original JSDoc AST. + * 3. Wait for all the promises to complete before exiting the script. + */ +(async () => { + return Promise.all( + (await readdir("src/conductor/plugins/autocomplete/builtins")) + .filter(file => file.endsWith(".json")) + .map(async file => { + const data = (await import(`./conductor/plugins/autocomplete/builtins/${file}`, { + with: { type: "json" }, + })) as { + default: Entry[]; + }; + await writeFile( + `src/conductor/plugins/autocomplete/builtins/${file}`, + JSON.stringify( + data.default + .filter(entry => entry.kind === "function" || entry.kind === "constant") + .map(entry => + entry.kind === "function" + ? { + name: entry.name, + title: + entry.name + + "(" + + entry.params.map(p => p.name).join(", ") + + ") -> " + + (entry.returns?.[0].type.names[0] || "None"), + description: entry.description, + meta: "func", + } + : { + name: entry.name, + title: entry.name, + description: entry.description, + meta: "var", + }, + ), + ), + ); + }), + ); +})(); diff --git a/yarn.lock b/yarn.lock index 35e156e1..4b97268c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -845,6 +845,42 @@ __metadata: languageName: node linkType: hard +"@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.2.0, @lezer/common@npm:^1.3.0": + version: 1.5.1 + resolution: "@lezer/common@npm:1.5.1" + checksum: 10c0/49baefdfc6f2244ad4f7d4a318149729fbecfd634fe1f7769883b5098ab9b35429140851e524c3a97614594004d8a3ad08fdd91221a63438be8c31ff2431fb54 + languageName: node + linkType: hard + +"@lezer/highlight@npm:^1.0.0": + version: 1.2.3 + resolution: "@lezer/highlight@npm:1.2.3" + dependencies: + "@lezer/common": "npm:^1.3.0" + checksum: 10c0/3bcb4fce7a1a45b5973895d7cb2be47970a0098700f2a0970aef9878ffd37f540285a2d7388ec1f524726ec90cc5196b5701bbb9764b7e7300786d772b7d2ce2 + languageName: node + linkType: hard + +"@lezer/lr@npm:^1.0.0": + version: 1.4.8 + resolution: "@lezer/lr@npm:1.4.8" + dependencies: + "@lezer/common": "npm:^1.0.0" + checksum: 10c0/8bd2228a316a5ef8da01908e3e22aca95fa9695211ffe56f3e8be756b37d0810d5aa91fbbdd274b198a343051d8637e130e26f51161161f089244af242b653c9 + languageName: node + linkType: hard + +"@lezer/python@npm:^1.1.18": + version: 1.1.18 + resolution: "@lezer/python@npm:1.1.18" + dependencies: + "@lezer/common": "npm:^1.2.0" + "@lezer/highlight": "npm:^1.0.0" + "@lezer/lr": "npm:^1.0.0" + checksum: 10c0/8d984729e887808c75800f18ed54560adfd4e67094b301a1666bdcd49e8987ab45f04c515563a92dfb1377d4a04dcf6616adc50a75285afe9ab53ab90f659bd5 + languageName: node + linkType: hard + "@npmcli/agent@npm:^4.0.0": version: 4.0.0 resolution: "@npmcli/agent@npm:4.0.0" @@ -1161,10 +1197,10 @@ __metadata: languageName: node linkType: hard -"@sourceacademy/conductor@npm:^0.2.3": - version: 0.2.3 - resolution: "@sourceacademy/conductor@npm:0.2.3" - checksum: 10c0/f6251017a6ed95ea4854a67f6b302934e1023bb256e15f93da8b1a55a95df082d88ba93813b5476445f4f82825dd2b7f44cdb1b20ae7815f9f8a8e6eb7816621 +"@sourceacademy/conductor@npm:^0.3.0": + version: 0.3.0 + resolution: "@sourceacademy/conductor@npm:0.3.0" + checksum: 10c0/e6d50cc0b9188fa5ec54c3bb2e6b9a0f9391ba37449e74a326c89c4e14deb00c4882700062427737e6f6fca556fd95d91fa9d7f6e125fe1d43938a65cc3c0c4d languageName: node linkType: hard @@ -4276,11 +4312,12 @@ __metadata: version: 0.0.0-use.local resolution: "py-slang@workspace:." dependencies: + "@lezer/python": "npm:^1.1.18" "@rollup/plugin-commonjs": "npm:^28.0.3" "@rollup/plugin-json": "npm:^6.1.0" "@rollup/plugin-node-resolve": "npm:^16.0.1" "@rollup/plugin-typescript": "npm:^12.1.2" - "@sourceacademy/conductor": "npm:^0.2.3" + "@sourceacademy/conductor": "npm:^0.3.0" "@sourceacademy/wasm-util": "npm:^1.0.4" "@types/fast-levenshtein": "npm:^0.0.4" "@types/jest": "npm:^29.5.14" From a99f731a947f158318e607c5ca59fffc8ec68fc2 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Tue, 24 Mar 2026 13:38:29 +0800 Subject: [PATCH 02/19] Add some more documentation --- .../plugins/autocomplete/resolver.ts | 53 ++++++++++++++++--- src/conductor/plugins/autocomplete/types.ts | 2 +- 2 files changed, 46 insertions(+), 9 deletions(-) diff --git a/src/conductor/plugins/autocomplete/resolver.ts b/src/conductor/plugins/autocomplete/resolver.ts index 584823ca..ee850fbd 100644 --- a/src/conductor/plugins/autocomplete/resolver.ts +++ b/src/conductor/plugins/autocomplete/resolver.ts @@ -17,11 +17,17 @@ const getNodeText = (node: SyntaxNode, doc: string): string => { function isCompletionItemKind(value: string): value is CompletionItemKind { return Object.values(CompletionItemKind).includes(value as CompletionItemKind); } -export const extractEnvironment = ( - iter: TreeCursor, - pos: number, - doc: string, -): Environment | null => { + +/** + * The extractEnvironment function traverses the syntax tree from the given position downwards to collect variable and function names in scope. + * It returns an Environment object representing the current scope and its child scope unless the position is inside a function parameter list, in which case it returns null. + * + * @param iter The TreeCursor used to traverse the syntax tree + * @param pos The 0-based index position in the document for which to extract the environment + * @param doc The document text, used to extract variable and function names from the syntax nodes + * @returns The Environment object representing the current scope and its child scopes, or null if the position is inside a function parameter list + */ +const extractEnvironment = (iter: TreeCursor, pos: number, doc: string): Environment | null => { const topEnv: Environment = { variables: [], functions: [], @@ -91,6 +97,12 @@ export const extractEnvironment = ( return topEnv; }; +/** + * Checks if `sub` is a subsequence of `str`, meaning all characters of `sub` appear in `str` in the same order, but not necessarily contiguously. + * @param sub The subsequence to check + * @param str The string to check against + * @returns true if `sub` is a subsequence of `str`, false otherwise + */ const isSubsequence = (sub: string, str: string): boolean => { let i = 0; for (const char of str) { @@ -104,6 +116,15 @@ const isSubsequence = (sub: string, str: string): boolean => { return false; }; +/** + * Converts a 1-based line and column number to a 0-based index in the document string. If the line or column is out of bounds, + * it returns the closest valid index (e.g. end of document). + * + * @param doc The document text + * @param line The 1-based line number + * @param column The 1-based column number + * @returns The 0-based index in the document string corresponding to the given line and column, or the closest valid index if out of bounds + */ const convertPosToIndex = (doc: string, line: number, column: number): number => { let pos = 0; while (line > 0) { @@ -117,6 +138,16 @@ const convertPosToIndex = (doc: string, line: number, column: number): number => return pos + column; }; +/** + * Gets the names of variables and functions in scope at the given position in the document, + * as well as built-in functions, variables and keywords. + * @param tree The syntax tree of the document, created using Lezer + * @param doc The document text + * @param line The 1-based line number of the cursor position + * @param column The 1-based column number of the cursor position + * @param variant The variant of the language + * @returns a list of autocomplete entries, each containing the name, type (variable or function), and documentation (for built-ins) + */ export const getNames = ( tree: Tree, doc: string, @@ -124,8 +155,8 @@ export const getNames = ( column: number, _variant: number, ): AutoCompleteEntry[] => { - const pos = convertPosToIndex(doc, line - 1, column); - const node = tree.resolve(pos, -1); + const pos = convertPosToIndex(doc, line - 1, column); // Convert position to 0-based index + const node = tree.resolve(pos, -1); // Get the syntax node ending at the cursor position if (node.type.name !== "VariableName") { return []; } @@ -133,13 +164,15 @@ export const getNames = ( const query = getNodeText(node, doc); let env: Environment | null = extractEnvironment(tree.cursor(), pos, doc); + const entries: AutoCompleteEntry[] = []; - let score = 1; + let score = 1; // The score is used to prioritize suggestions from inner scopes over outer scopes. Built-ins will have the lowest score. while (env) { const symbols = [ ...env.variables.map(v => ({ name: v, meta: CompletionItemKind.Variable, score: score })), ...env.functions.map(f => ({ name: f, meta: CompletionItemKind.Function, score: score })), ]; + // Filter symbols based on query, sort alphabetically, and add to entries symbols .filter(s => isSubsequence(query, s.name)) .sort((a, b) => a.name.localeCompare(b.name)) @@ -148,6 +181,10 @@ export const getNames = ( env = env.child; score++; } + + // TODO: Add keywords to autocomplete suggestions + // TODO: Add docstrings for user-defined functions to autocomplete suggestions? + // TODO: Add documentation for otherbuilt-ins, not just math and misc modules const symbols = [...miscJSON, ...mathJSON].map(v => ({ name: v.name, meta: isCompletionItemKind(v.meta) ? v.meta : CompletionItemKind.Variable, diff --git a/src/conductor/plugins/autocomplete/types.ts b/src/conductor/plugins/autocomplete/types.ts index 2da0e82a..81ff669f 100644 --- a/src/conductor/plugins/autocomplete/types.ts +++ b/src/conductor/plugins/autocomplete/types.ts @@ -62,7 +62,7 @@ export type SyntaxHighlightMessage = } | { type: "ack" }; -// Lifted from https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.completionitemkind?view=visualstudiosdk-2022 +// Adapted from https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.completionitemkind?view=visualstudiosdk-2022 export enum CompletionItemKind { Text = "text", Method = "method", From f191853af4a7c64305c82d44ab76df57c72b0c88 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Tue, 24 Mar 2026 15:13:04 +0800 Subject: [PATCH 03/19] Added keywords --- .../plugins/autocomplete/highlight-rules.ts | 21 +++----- src/conductor/plugins/autocomplete/index.ts | 7 ++- .../plugins/autocomplete/keywords.ts | 48 +++++++++++++++++++ src/conductor/plugins/autocomplete/mode.ts | 10 ++-- .../plugins/autocomplete/resolver.ts | 14 ++++-- 5 files changed, 73 insertions(+), 27 deletions(-) create mode 100644 src/conductor/plugins/autocomplete/keywords.ts diff --git a/src/conductor/plugins/autocomplete/highlight-rules.ts b/src/conductor/plugins/autocomplete/highlight-rules.ts index d8ac65fa..6c855412 100644 --- a/src/conductor/plugins/autocomplete/highlight-rules.ts +++ b/src/conductor/plugins/autocomplete/highlight-rules.ts @@ -1,9 +1,9 @@ /** * Adapted from https://github.com/ajaxorg/ace/blob/master/src/mode/python_highlight_rules.js - * + * * Copyright (c) 2010, Ajax.org B.V. * All rights reserved. - * + * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * Redistributions of source code must retain the above copyright @@ -25,24 +25,15 @@ * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - * + * */ import constants from "../../../stdlib/py_s1_constants.json"; +import { getIllegalKeywords, getKeywords } from "./keywords"; import { AceRules } from "./types"; export default (variant: number) => { - let keywords = - "and|def|elif|else|from|global|if|import|lambda|not|or|pass|return|nonlocal"; - - let illegalKeywords = - "as|assert|class|del|except|finally|is|match|case|raise|try|with|yield|async|await"; - - if (variant < 3) { - illegalKeywords += "|while|for|break|continue|is"; - } else { - keywords += "|while|for|break|continue"; - } - + const keywords = getKeywords(variant).join("|"); + const illegalKeywords = getIllegalKeywords(variant).join("|"); const builtinConstants = constants.constants.join("|"); diff --git a/src/conductor/plugins/autocomplete/index.ts b/src/conductor/plugins/autocomplete/index.ts index 9069f231..24c92682 100644 --- a/src/conductor/plugins/autocomplete/index.ts +++ b/src/conductor/plugins/autocomplete/index.ts @@ -6,10 +6,9 @@ import pythonMode from "./mode"; import { getNames } from "./resolver"; import type { AutoCompleteMessage, SyntaxHighlightMessage } from "./types"; - /** - * This plugin provides autocomplete suggestions and syntax highlighting for Python code. - * + * This plugin provides autocomplete suggestions and syntax highlighting for Python code. + * * It provides two channels: one for autocomplete requests and responses, and another for sending syntax highlighting information to the web plugin. * a) The autocomplete channel listens for requests containing the current code and cursor position. It uses the resolver to find relevant symbols based on the cursor position and sends back a response with the autocomplete suggestions. * b) The syntax highlighting channel periodically sends the Python mode information to the web plugin until it receives an acknowledgment, ensuring that the web plugin has the necessary information to perform syntax highlighting. @@ -32,7 +31,7 @@ export default class AutoCompletePlugin implements IPlugin { const handler = setInterval(() => { this.__syntaxHighlightChannel.send({ type: "message", - data: pythonMode, + data: pythonMode(variant), }); }, 1000); this.__syntaxHighlightChannel.subscribe((message: SyntaxHighlightMessage) => { diff --git a/src/conductor/plugins/autocomplete/keywords.ts b/src/conductor/plugins/autocomplete/keywords.ts new file mode 100644 index 00000000..9ab990f4 --- /dev/null +++ b/src/conductor/plugins/autocomplete/keywords.ts @@ -0,0 +1,48 @@ +export const getKeywords = (variant: number): string[] => { + let keywords = [ + "and", + "def", + "elif", + "else", + "from", + "global", + "if", + "import", + "lambda", + "not", + "or", + "pass", + "return", + "nonlocal", + ]; + + if (variant >= 3) { + keywords = keywords.concat(["while", "for", "break", "continue"]); + } + return keywords; +}; + +export const getIllegalKeywords = (variant: number): string[] => { + let illegalKeywords = [ + "as", + "assert", + "class", + "del", + "except", + "finally", + "is", + "match", + "case", + "raise", + "try", + "with", + "yield", + "async", + "await", + ]; + + if (variant < 3) { + illegalKeywords = illegalKeywords.concat(["while", "for", "break", "continue"]); + } + return illegalKeywords; +}; diff --git a/src/conductor/plugins/autocomplete/mode.ts b/src/conductor/plugins/autocomplete/mode.ts index 847984b5..575d0b80 100644 --- a/src/conductor/plugins/autocomplete/mode.ts +++ b/src/conductor/plugins/autocomplete/mode.ts @@ -1,6 +1,6 @@ import PythonHighlightRules from "./highlight-rules"; -export default { - highlightRules: PythonHighlightRules(1), +export default (variant: number) => ({ + highlightRules: PythonHighlightRules(variant), foldingRules: { hookFrom: "ace/mode/folding/pythonic", args: ["\\:"], @@ -19,6 +19,6 @@ export default { autoOutdent: { hookFrom: "ace/mode/python", }, - id: "ace/mode/pythonBetaDefault", - snippetFileId: "ace/snippets/pythonBetaDefault", -}; + id: "ace/mode/python" + variant, + snippetFileId: "ace/snippets/python" + variant, +}); diff --git a/src/conductor/plugins/autocomplete/resolver.ts b/src/conductor/plugins/autocomplete/resolver.ts index ee850fbd..fbfb98bb 100644 --- a/src/conductor/plugins/autocomplete/resolver.ts +++ b/src/conductor/plugins/autocomplete/resolver.ts @@ -3,6 +3,7 @@ import { AutoCompleteEntry, CompletionItemKind } from "./types"; import mathJSON from "./builtins/math.json"; import miscJSON from "./builtins/misc.json"; +import { getKeywords } from "./keywords"; type Environment = { variables: string[]; @@ -153,7 +154,7 @@ export const getNames = ( doc: string, line: number, column: number, - _variant: number, + variant: number, ): AutoCompleteEntry[] => { const pos = convertPosToIndex(doc, line - 1, column); // Convert position to 0-based index const node = tree.resolve(pos, -1); // Get the syntax node ending at the cursor position @@ -182,9 +183,16 @@ export const getNames = ( score++; } - // TODO: Add keywords to autocomplete suggestions + getKeywords(variant) + .map(k => ({ + name: k, + meta: CompletionItemKind.Keyword, + score: score, // Keywords are given the highest score + })) + .forEach(s => entries.push(s)); + // TODO: Add docstrings for user-defined functions to autocomplete suggestions? - // TODO: Add documentation for otherbuilt-ins, not just math and misc modules + // TODO: Add documentation for other built-ins, not just math and misc modules const symbols = [...miscJSON, ...mathJSON].map(v => ({ name: v.name, meta: isCompletionItemKind(v.meta) ? v.meta : CompletionItemKind.Variable, From 528c3268b7a1c0636bc206345fcc33b80afcba02 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Tue, 24 Mar 2026 15:56:29 +0800 Subject: [PATCH 04/19] Add autocomplete for all groups based on variant + Fix bug where variables in inner scopes didn't work --- .../plugins/autocomplete/resolver.ts | 41 ++++++++++--------- src/generate-autocomplete.mts | 1 + 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/conductor/plugins/autocomplete/resolver.ts b/src/conductor/plugins/autocomplete/resolver.ts index fbfb98bb..1b113508 100644 --- a/src/conductor/plugins/autocomplete/resolver.ts +++ b/src/conductor/plugins/autocomplete/resolver.ts @@ -1,8 +1,12 @@ import { SyntaxNode, Tree, TreeCursor } from "@lezer/common"; import { AutoCompleteEntry, CompletionItemKind } from "./types"; +import linkedListJSON from "./builtins/linked_list.json"; +import listJSON from "./builtins/list.json"; import mathJSON from "./builtins/math.json"; import miscJSON from "./builtins/misc.json"; +import pairmutatorJSON from "./builtins/pairmutator.json"; +import streamJSON from "./builtins/stream.json"; import { getKeywords } from "./keywords"; type Environment = { @@ -36,7 +40,7 @@ const extractEnvironment = (iter: TreeCursor, pos: number, doc: string): Environ }; let currentEnv = topEnv; - while (iter) { + do { if (iter.node.type.name === "ParamList") { return null; } @@ -50,15 +54,9 @@ const extractEnvironment = (iter: TreeCursor, pos: number, doc: string): Environ } } } - if (!iter.enter(pos, -1)) { - break; - } continue; } - if (iter.node.type.name !== "Block" && iter.node.type.name !== "Script") { - if (!iter.enter(pos, -1)) { - break; - } + if (iter.node.type.name !== "Body" && iter.node.type.name !== "Script") { continue; } // Iterate children @@ -90,11 +88,7 @@ const extractEnvironment = (iter: TreeCursor, pos: number, doc: string): Environ }; currentEnv.child = nextEnv; currentEnv = nextEnv; - - if (!iter.enter(pos, -1)) { - break; - } - } + } while (iter.enter(pos, -1)); return topEnv; }; @@ -192,13 +186,22 @@ export const getNames = ( .forEach(s => entries.push(s)); // TODO: Add docstrings for user-defined functions to autocomplete suggestions? - // TODO: Add documentation for other built-ins, not just math and misc modules - const symbols = [...miscJSON, ...mathJSON].map(v => ({ - name: v.name, - meta: isCompletionItemKind(v.meta) ? v.meta : CompletionItemKind.Variable, - docHTML: "

" + v.title + "

" + v.description + "

", - })); + const symbols = [...miscJSON, ...mathJSON]; + if (variant >= 2) { + symbols.push(...linkedListJSON); + } + if (variant >= 3) { + symbols.push(...listJSON); + symbols.push(...pairmutatorJSON); + symbols.push(...streamJSON); + } + console.log(symbols); symbols + .map(v => ({ + name: v.name, + meta: isCompletionItemKind(v.meta) ? v.meta : CompletionItemKind.Variable, + docHTML: "

" + v.title + "

" + v.description + "

", + })) .filter(s => isSubsequence(query, s.name)) .sort((a, b) => a.name.localeCompare(b.name)) .forEach(s => entries.push(s)); diff --git a/src/generate-autocomplete.mts b/src/generate-autocomplete.mts index a9ec9e32..54e1ef06 100644 --- a/src/generate-autocomplete.mts +++ b/src/generate-autocomplete.mts @@ -79,6 +79,7 @@ type Entry = JSON.stringify( data.default .filter(entry => entry.kind === "function" || entry.kind === "constant") + .filter(entry => entry.scope === "global") // Only include global variables and functions in autocomplete suggestions .map(entry => entry.kind === "function" ? { From 8f05bb121df7722d0509993ac8bf9a33a404ffce Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Tue, 24 Mar 2026 19:05:38 +0800 Subject: [PATCH 05/19] Add for statements + Fix function names and name declarations coming as their own suggestion --- src/conductor/plugins/autocomplete/resolver.ts | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/conductor/plugins/autocomplete/resolver.ts b/src/conductor/plugins/autocomplete/resolver.ts index 1b113508..3e8060e6 100644 --- a/src/conductor/plugins/autocomplete/resolver.ts +++ b/src/conductor/plugins/autocomplete/resolver.ts @@ -44,6 +44,12 @@ const extractEnvironment = (iter: TreeCursor, pos: number, doc: string): Environ if (iter.node.type.name === "ParamList") { return null; } + if (iter.node.type.name === "ForStatement") { + const target = iter.node.getChild("VariableName"); + if (target) { + currentEnv.variables.push(getNodeText(target, doc)); + } + } if (iter.node.type.name == "FunctionDefinition" || iter.node.type.name == "LambdaExpression") { // Add function parameters to inner environment const params = iter.node.getChild("ParamList"); @@ -89,6 +95,12 @@ const extractEnvironment = (iter: TreeCursor, pos: number, doc: string): Environ currentEnv.child = nextEnv; currentEnv = nextEnv; } while (iter.enter(pos, -1)); + if ( + iter.node.parent && + ["FunctionDefinition", "ForStatement"].includes(iter.node.parent.type.name) + ) { + return null; + } return topEnv; }; @@ -195,7 +207,6 @@ export const getNames = ( symbols.push(...pairmutatorJSON); symbols.push(...streamJSON); } - console.log(symbols); symbols .map(v => ({ name: v.name, From ae69e326df66e287227de41320676000d1795da4 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Fri, 27 Mar 2026 17:11:34 +0800 Subject: [PATCH 06/19] Migrate to `@sourceacademy/autocomplete` --- package.json | 1 + .../plugins/autocomplete/constants.ts | 5 - .../plugins/autocomplete/highlight-rules.ts | 3 +- src/conductor/plugins/autocomplete/index.ts | 63 ++++-------- src/conductor/plugins/autocomplete/mode.ts | 3 +- .../plugins/autocomplete/resolver.ts | 2 +- src/conductor/plugins/autocomplete/types.ts | 99 ------------------- src/generate-autocomplete.mts | 5 +- yarn.lock | 10 ++ 9 files changed, 40 insertions(+), 151 deletions(-) delete mode 100644 src/conductor/plugins/autocomplete/constants.ts delete mode 100644 src/conductor/plugins/autocomplete/types.ts diff --git a/package.json b/package.json index 20c475e6..ad4696a7 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "@rollup/plugin-json": "^6.1.0", "@rollup/plugin-node-resolve": "^16.0.1", "@rollup/plugin-typescript": "^12.1.2", + "@sourceacademy/autocomplete": "github:source-academy/autocomplete#0.0.1", "@types/fast-levenshtein": "^0.0.4", "@types/jest": "^29.5.14", "@types/moo": "^0.5.10", diff --git a/src/conductor/plugins/autocomplete/constants.ts b/src/conductor/plugins/autocomplete/constants.ts deleted file mode 100644 index bc0faa28..00000000 --- a/src/conductor/plugins/autocomplete/constants.ts +++ /dev/null @@ -1,5 +0,0 @@ -export const WEB_PLUGIN_ID = "__autocomplete_plugin_web"; -export const RUNNER_PLUGIN_ID = "__autocomplete_plugin_runner"; - -export const AUTOCOMPLETE_CHANNEL_ID = "__autocomplete_channel__"; -export const SYNTAX_CHANNEL_ID = "__syntax_channel__"; diff --git a/src/conductor/plugins/autocomplete/highlight-rules.ts b/src/conductor/plugins/autocomplete/highlight-rules.ts index 6c855412..040c82c3 100644 --- a/src/conductor/plugins/autocomplete/highlight-rules.ts +++ b/src/conductor/plugins/autocomplete/highlight-rules.ts @@ -28,9 +28,10 @@ * */ +import { AceRules } from "@sourceacademy/autocomplete"; import constants from "../../../stdlib/py_s1_constants.json"; import { getIllegalKeywords, getKeywords } from "./keywords"; -import { AceRules } from "./types"; + export default (variant: number) => { const keywords = getKeywords(variant).join("|"); const illegalKeywords = getIllegalKeywords(variant).join("|"); diff --git a/src/conductor/plugins/autocomplete/index.ts b/src/conductor/plugins/autocomplete/index.ts index 24c92682..30396f9d 100644 --- a/src/conductor/plugins/autocomplete/index.ts +++ b/src/conductor/plugins/autocomplete/index.ts @@ -1,54 +1,33 @@ import { parser } from "@lezer/python"; -import { IChannel, IConduit, IPlugin } from "@sourceacademy/conductor/conduit"; - -import { AUTOCOMPLETE_CHANNEL_ID, RUNNER_PLUGIN_ID, SYNTAX_CHANNEL_ID } from "./constants"; +import { + AutoCompleteEntry, + BaseAutoCompleteRunnerPlugin, + SyntaxHighlightData, +} from "@sourceacademy/autocomplete"; +import { IChannel, IConduit } from "@sourceacademy/conductor/conduit"; import pythonMode from "./mode"; import { getNames } from "./resolver"; -import type { AutoCompleteMessage, SyntaxHighlightMessage } from "./types"; - -/** - * This plugin provides autocomplete suggestions and syntax highlighting for Python code. - * - * It provides two channels: one for autocomplete requests and responses, and another for sending syntax highlighting information to the web plugin. - * a) The autocomplete channel listens for requests containing the current code and cursor position. It uses the resolver to find relevant symbols based on the cursor position and sends back a response with the autocomplete suggestions. - * b) The syntax highlighting channel periodically sends the Python mode information to the web plugin until it receives an acknowledgment, ensuring that the web plugin has the necessary information to perform syntax highlighting. - */ -export default class AutoCompletePlugin implements IPlugin { - static readonly channelAttach = [AUTOCOMPLETE_CHANNEL_ID, SYNTAX_CHANNEL_ID]; - readonly id: string = RUNNER_PLUGIN_ID; - - private readonly __autoCompleteChannel: IChannel; - private readonly __syntaxHighlightChannel: IChannel; +export default class AutoCompletePlugin extends BaseAutoCompleteRunnerPlugin { + private readonly variant: number; constructor( _conduit: IConduit, // eslint-disable-next-line @typescript-eslint/no-explicit-any - [autoCompleteChannel, syntaxHighlightChannel]: IChannel[], + channels: IChannel[], variant: number, ) { - this.__autoCompleteChannel = autoCompleteChannel; - this.__syntaxHighlightChannel = syntaxHighlightChannel; - const handler = setInterval(() => { - this.__syntaxHighlightChannel.send({ - type: "message", - data: pythonMode(variant), - }); - }, 1000); - this.__syntaxHighlightChannel.subscribe((message: SyntaxHighlightMessage) => { - if (message.type === "ack") { - clearInterval(handler); - } - }); - this.__autoCompleteChannel.subscribe((message: AutoCompleteMessage) => { - if (message.type === "request") { - const tree = parser.parse(message.code); + super(_conduit, channels); + this.variant = variant; + } + + get mode(): SyntaxHighlightData { + return pythonMode(this.variant); + } + + autocomplete(code: string, row: number, column: number): AutoCompleteEntry[] { + const tree = parser.parse(code); - const entries = getNames(tree, message.code, message.row, message.column, variant); - this.__autoCompleteChannel.send({ - type: "response", - declarations: entries, - }); - } - }); + const entries = getNames(tree, code, row, column, this.variant); + return entries; } } diff --git a/src/conductor/plugins/autocomplete/mode.ts b/src/conductor/plugins/autocomplete/mode.ts index 575d0b80..31b3c583 100644 --- a/src/conductor/plugins/autocomplete/mode.ts +++ b/src/conductor/plugins/autocomplete/mode.ts @@ -1,5 +1,6 @@ +import { SyntaxHighlightData } from "@sourceacademy/autocomplete"; import PythonHighlightRules from "./highlight-rules"; -export default (variant: number) => ({ +export default (variant: number): SyntaxHighlightData => ({ highlightRules: PythonHighlightRules(variant), foldingRules: { hookFrom: "ace/mode/folding/pythonic", diff --git a/src/conductor/plugins/autocomplete/resolver.ts b/src/conductor/plugins/autocomplete/resolver.ts index 3e8060e6..c781ffb8 100644 --- a/src/conductor/plugins/autocomplete/resolver.ts +++ b/src/conductor/plugins/autocomplete/resolver.ts @@ -1,5 +1,5 @@ import { SyntaxNode, Tree, TreeCursor } from "@lezer/common"; -import { AutoCompleteEntry, CompletionItemKind } from "./types"; +import { AutoCompleteEntry, CompletionItemKind } from "@sourceacademy/autocomplete"; import linkedListJSON from "./builtins/linked_list.json"; import listJSON from "./builtins/list.json"; diff --git a/src/conductor/plugins/autocomplete/types.ts b/src/conductor/plugins/autocomplete/types.ts deleted file mode 100644 index 81ff669f..00000000 --- a/src/conductor/plugins/autocomplete/types.ts +++ /dev/null @@ -1,99 +0,0 @@ -export type AutoCompleteRequest = { - type: "request"; - code: string; - row: number; - column: number; -}; -export type AutoCompleteResponse = { - type: "response"; - declarations: AutoCompleteEntry[]; -}; - -export type AutoCompleteMessage = AutoCompleteRequest | AutoCompleteResponse; - -export type SyntaxHighlightData = { - highlightRules: AceRules; - foldingRules: { - hookFrom: string; - args: string[]; - }; - lineCommentStart: string; - pairQuotesAfter: Record; - indents: { - hookFrom: string; - }; - outdents: { - hookFrom: string; - }; - autoOutdent: { - hookFrom: string; - }; - id: string; - snippetFileId: string; -}; - -export type KeywordMapperArgs = { - map: Record; - defaultToken: string; -}; - -export type AceRule = - | { - token: string | string[] | KeywordMapperArgs; - regex: string; - next?: string; - push?: string; - } - | { - include: string; - } - | { - defaultToken: string; - }; - -export type AceRules = { - [state: string]: AceRule[]; -}; - -export type SyntaxHighlightMessage = - | { - type: "message"; - data: SyntaxHighlightData; - } - | { type: "ack" }; - -// Adapted from https://learn.microsoft.com/en-us/dotnet/api/microsoft.visualstudio.languageserver.protocol.completionitemkind?view=visualstudiosdk-2022 -export enum CompletionItemKind { - Text = "text", - Method = "method", - Function = "func", - Constructor = "constructor", - Field = "field", - Variable = "var", - Class = "class", - Interface = "interface", - Module = "module", - Property = "property", - Unit = "unit", - Value = "value", - Enum = "enum", - Keyword = "keyword", - Snippet = "snippet", - Color = "color", - File = "file", - Reference = "reference", - Folder = "folder", - EnumMember = "enumMember", - Constant = "constant", - Struct = "struct", - Event = "event", - Operator = "operator", - TypeParameter = "typeParameter", -} - -export interface AutoCompleteEntry { - name: string; - meta: CompletionItemKind; - score?: number; - docHTML?: string; -} diff --git a/src/generate-autocomplete.mts b/src/generate-autocomplete.mts index 54e1ef06..10abfdac 100644 --- a/src/generate-autocomplete.mts +++ b/src/generate-autocomplete.mts @@ -1,3 +1,4 @@ +import { CompletionItemKind } from "@sourceacademy/autocomplete"; import { readdir, writeFile } from "node:fs/promises"; /** @@ -91,13 +92,13 @@ type Entry = ") -> " + (entry.returns?.[0].type.names[0] || "None"), description: entry.description, - meta: "func", + meta: CompletionItemKind.Function, } : { name: entry.name, title: entry.name, description: entry.description, - meta: "var", + meta: CompletionItemKind.Variable, }, ), ), diff --git a/yarn.lock b/yarn.lock index 4b97268c..cef35780 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1197,6 +1197,15 @@ __metadata: languageName: node linkType: hard +"@sourceacademy/autocomplete@github:source-academy/autocomplete#0.0.1": + version: 0.0.1 + resolution: "@sourceacademy/autocomplete@https://github.com/source-academy/autocomplete.git#commit=db3ba73eedcef82eff6b4031e423804b67a2bb6e" + peerDependencies: + "@sourceacademy/conductor": ">=0.3.0" + checksum: 10c0/0c836d923d5658071882b4125445237f99cbef97f2f0b2834f0b244587a5dabd44944e4c6cda766ad3458c6646a8302bf7354a513b7486ec174fb46809f4a08b + languageName: node + linkType: hard + "@sourceacademy/conductor@npm:^0.3.0": version: 0.3.0 resolution: "@sourceacademy/conductor@npm:0.3.0" @@ -4317,6 +4326,7 @@ __metadata: "@rollup/plugin-json": "npm:^6.1.0" "@rollup/plugin-node-resolve": "npm:^16.0.1" "@rollup/plugin-typescript": "npm:^12.1.2" + "@sourceacademy/autocomplete": "github:source-academy/autocomplete#0.0.1" "@sourceacademy/conductor": "npm:^0.3.0" "@sourceacademy/wasm-util": "npm:^1.0.4" "@types/fast-levenshtein": "npm:^0.0.4" From 555215697b8045ad7402a7bde1801c702cc590cb Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Mon, 13 Apr 2026 13:08:26 +0800 Subject: [PATCH 07/19] Update `ts-node` to `tsx` --- scripts/autocomplete.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/autocomplete.sh b/scripts/autocomplete.sh index e883abfc..33d09277 100755 --- a/scripts/autocomplete.sh +++ b/scripts/autocomplete.sh @@ -18,4 +18,4 @@ for file in "$LIB"/*.js; do done wait -yarn ts-node src/generate-autocomplete.mts +yarn tsx src/generate-autocomplete.mts From ebc291c28c8cc53616a7e86ce000a06a08c5cc44 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Mon, 13 Apr 2026 18:05:39 +0800 Subject: [PATCH 08/19] Use `.kind` instead of `.constructor.name` to allow function name terserification --- rollup.config.mjs | 1 - src/engines/cse/environment.ts | 6 +++--- src/engines/cse/interpreter.ts | 18 +++++++++--------- src/engines/cse/types.ts | 2 +- src/engines/cse/utils.ts | 4 ++-- 5 files changed, 15 insertions(+), 16 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index 542d0dd3..5f1f44b0 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -27,7 +27,6 @@ function plugins() { nodePolyfills(), terser({ compress: { dead_code: true, passes: 3 }, - keep_fnames: true, }), ]; } diff --git a/src/engines/cse/environment.ts b/src/engines/cse/environment.ts index 20ac62be..47665804 100644 --- a/src/engines/cse/environment.ts +++ b/src/engines/cse/environment.ts @@ -1,4 +1,4 @@ -import { ExprNS, StmtNS } from "../../ast-types"; +import { ExprNS } from "../../ast-types"; import { MissingRequiredPositionalError, TooManyPositionalArgumentsError } from "../../errors"; import { Closure } from "./closure"; import { Context } from "./context"; @@ -60,8 +60,8 @@ export const createEnvironment = ( ): Environment => { const environment: Environment = { name: - closure.node.constructor.name === "FunctionDef" - ? (closure.node as StmtNS.FunctionDef).name.lexeme + closure.node.kind === "FunctionDef" + ? closure.node.name.lexeme : "lambda", tail: closure.environment, head: {}, diff --git a/src/engines/cse/interpreter.ts b/src/engines/cse/interpreter.ts index 70e9744d..20aed36f 100644 --- a/src/engines/cse/interpreter.ts +++ b/src/engines/cse/interpreter.ts @@ -290,7 +290,7 @@ export async function* generateCSEMachineStateStream( if (isNode(command)) { const node = command as Node; - const nodeType = node.constructor.name; + const nodeType = node.kind; context.runtime.nodes.shift(); context.runtime.nodes.unshift(command); @@ -665,7 +665,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { ) { const whileNode = command as StmtNS.While; const instr = instrCreator.whileInstr(whileNode, whileNode.condition, { - type: "StatementSequence", + kind: "StatementSequence", body: whileNode.body, }); control.push(instr); @@ -704,11 +704,11 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { ) { const ifNode = command as StmtNS.If; const branch = instrCreator.branchInstr( - { type: "StatementSequence", body: ifNode.body }, + { kind: "StatementSequence", body: ifNode.body }, ifNode.elseBlock ? Array.isArray(ifNode.elseBlock) ? // 'else' block - { type: "StatementSequence", body: ifNode.elseBlock } + { kind: "StatementSequence", body: ifNode.elseBlock } : // 'elif' block ifNode.elseBlock : // 'else' block dont exist @@ -1065,8 +1065,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { if (callable?.type == "closure") { const closure = callable.closure; control.push(instrCreator.resetInstr(instr.srcNode)); - - if (closure.node.constructor.name === "FunctionDef") { + if (closure.node.kind === "FunctionDef") { control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode)); } @@ -1074,13 +1073,14 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { pushEnvironment(context, newEnv); const closureNode = closure.node; - if (closureNode.constructor.name === "FunctionDef") { - const bodyStmts = (closureNode as StmtNS.FunctionDef).body.slice().reverse(); + if (closureNode.kind === "FunctionDef") { + const bodyStmts = closureNode.body.slice().reverse(); control.push(...bodyStmts); } else { - const bodyExpr = (closureNode as ExprNS.Lambda).body; + const bodyExpr = closureNode.body; control.push(bodyExpr); } + console.log(control); } else if (callable?.type === "builtin") { const result = await callable.func(args, code, instr.srcNode, context); stash.push(result); diff --git a/src/engines/cse/types.ts b/src/engines/cse/types.ts index 37bf26c0..346ddc1f 100644 --- a/src/engines/cse/types.ts +++ b/src/engines/cse/types.ts @@ -6,7 +6,7 @@ import { Value } from "./stash"; export type Node = { isEnvDependent?: boolean } & (StmtNS.Stmt | ExprNS.Expr | StatementSequence); export interface StatementSequence { - type: "StatementSequence"; + kind: "StatementSequence"; body: StmtNS.Stmt[]; loc?: { start: { line: number; column: number }; diff --git a/src/engines/cse/utils.ts b/src/engines/cse/utils.ts index e7ba9fe4..cd3ae5f6 100644 --- a/src/engines/cse/utils.ts +++ b/src/engines/cse/utils.ts @@ -235,7 +235,7 @@ export function isEnvDependent(item: ControlItem | null | undefined): boolean { } let setter: Transformer | undefined; if (isNode(item)) { - const key = "type" in item && typeof item.type === "string" ? item.type : item.constructor.name; + const key = item.kind; setter = propertySetter.get(key); } else if (isInstr(item)) { setter = propertySetter.get(item.instrType); @@ -367,7 +367,7 @@ export function scanForAssignments(node: Node | Node[]): Set { return; } - const nodeType = curNode.constructor.name; + const nodeType = curNode.kind; if (nodeType === "Assign") { const assignNode = curNode as StmtNS.Assign; From 88403777dfc6bf84bc1a24b74220adaf959dde70 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Mon, 13 Apr 2026 19:40:25 +0800 Subject: [PATCH 09/19] Fix the step limit --- src/engines/cse/closure.ts | 2 +- src/engines/cse/control.ts | 5 +---- src/engines/cse/interpreter.ts | 7 +++---- src/errors/errors.ts | 18 ++++++++++++++---- 4 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/engines/cse/closure.ts b/src/engines/cse/closure.ts index 13382336..d15f38bb 100644 --- a/src/engines/cse/closure.ts +++ b/src/engines/cse/closure.ts @@ -65,5 +65,5 @@ export class Closure { } export const isStatementSequence = (node: ControlItem): node is StatementSequence => { - return (node as StatementSequence).type == "StatementSequence"; + return (node as StatementSequence).kind == "StatementSequence"; }; diff --git a/src/engines/cse/control.ts b/src/engines/cse/control.ts index 64b62d1a..63d4826b 100644 --- a/src/engines/cse/control.ts +++ b/src/engines/cse/control.ts @@ -3,10 +3,7 @@ import { Stack } from "./stack"; import { Instr, Node } from "./types"; import { isEnvDependent } from "./utils"; -export type ControlItem = (Node | Instr) & { - isEnvDependent?: boolean; - skipEnv?: boolean; -}; +export type ControlItem = Node | Instr; /** * The control stores the remaining instructions to be executed diff --git a/src/engines/cse/interpreter.ts b/src/engines/cse/interpreter.ts index 20aed36f..4d67784b 100644 --- a/src/engines/cse/interpreter.ts +++ b/src/engines/cse/interpreter.ts @@ -116,7 +116,7 @@ export async function evaluate( isPrelude: false, groups: [], envSteps: 100000, - stepLimit: 100000, + stepLimit: -1, variant: 4, ...(options as Partial), }; @@ -278,7 +278,7 @@ export async function* generateCSEMachineStateStream( // Step limit reached, stop further evaluation if (!isPrelude && steps === stepLimit) { - handleRuntimeError(context, new error.StepLimitExceededError(source, command as ExprNS.Expr)); + handleRuntimeError(context, new error.StepLimitExceededError(source, command)); } if (!isPrelude && envChanging(command)) { @@ -289,7 +289,7 @@ export async function* generateCSEMachineStateStream( control.pop(); if (isNode(command)) { - const node = command as Node; + const node = command; const nodeType = node.kind; context.runtime.nodes.shift(); @@ -1080,7 +1080,6 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { const bodyExpr = closureNode.body; control.push(bodyExpr); } - console.log(control); } else if (callable?.type === "builtin") { const result = await callable.func(args, code, instr.srcNode, context); stash.push(result); diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 31b69eb4..72e010cd 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -1,6 +1,9 @@ import { ExprNS, StmtNS } from "../ast-types"; +import { isStatementSequence } from "../engines/cse/closure"; import { Context } from "../engines/cse/context"; +import { ControlItem } from "../engines/cse/control"; import { operatorTranslator } from "../engines/cse/types"; +import { isNode } from "../engines/cse/utils"; import { Token } from "../tokenizer"; import { TokenType } from "../tokens"; @@ -377,15 +380,22 @@ export class ZeroDivisionError extends RuntimeSourceError { } export class StepLimitExceededError extends RuntimeSourceError { - constructor(source: string, node: ExprNS.Binary | ExprNS.Expr) { - super(node); + constructor(source: string, node: ControlItem) { + const srcNode = isNode(node) ? node : node.srcNode; + const locatable = isStatementSequence(srcNode) + ? srcNode.body[0] + : srcNode; + + super(locatable); this.type = ErrorType.RUNTIME; - const index = node.startToken.indexInSource; + const index = locatable.startToken.indexInSource; const { lineIndex, fullLine } = getFullLine(source, index); const errorPos = - "operator" in node ? node.operator.indexInSource - node.startToken.indexInSource : 0; + "operator" in locatable && locatable.operator instanceof Token + ? locatable.operator.indexInSource - locatable.startToken.indexInSource + : 0; const indicator = createErrorIndicator(fullLine, errorPos); // no target symbol From f426caa95a4bcf4d134d0c82969e60e5a4ae66d2 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Mon, 13 Apr 2026 20:50:22 +0800 Subject: [PATCH 10/19] Refactor type narrowing --- scripts/build.ts | 4 +- src/engines/cse/closure.ts | 3 +- src/engines/cse/instrCreator.ts | 2 +- src/engines/cse/interpreter.ts | 41 +-- src/engines/cse/operators.ts | 19 +- src/engines/cse/stash.ts | 7 +- src/engines/cse/types.ts | 2 +- src/engines/cse/utils.ts | 5 +- src/engines/svml/svml-interpreter.ts | 6 +- src/parser/lexer.ts | 10 +- src/parser/parser-adapter.ts | 2 +- src/resolver/resolver.ts | 8 +- src/stdlib.ts | 378 +++++++++++++-------------- src/stdlib/linked-list.ts | 40 +-- src/stdlib/list.ts | 12 +- src/stdlib/pairmutator.ts | 10 +- src/stdlib/parser.ts | 147 +++++------ src/stdlib/stream.ts | 8 +- 18 files changed, 342 insertions(+), 362 deletions(-) diff --git a/scripts/build.ts b/scripts/build.ts index c30b5817..2f793885 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -14,7 +14,7 @@ const allTargets = [ "PySvmlSinterEvaluator", ] as const; -type EvaluatorName = string; +type EvaluatorName = typeof allTargets[number]; function buildTarget(target: EvaluatorName, extraArgs: string[] = []): Promise { console.log(`\nBuilding ${target}...\n`); @@ -61,7 +61,7 @@ async function resolveTargets(evaluator?: string, all?: boolean): Promise { - return (node as StatementSequence).kind == "StatementSequence"; + return isNode(node) && node.kind == "StatementSequence"; }; diff --git a/src/engines/cse/instrCreator.ts b/src/engines/cse/instrCreator.ts index 5d0a603e..36377683 100644 --- a/src/engines/cse/instrCreator.ts +++ b/src/engines/cse/instrCreator.ts @@ -40,7 +40,7 @@ export const assmtInstr = ( export const appInstr = ( numOfArgs: number, - srcNode: Node, + srcNode: ExprNS.Call, spreadIndices: number[] = [], ): AppInstr => ({ instrType: InstrType.APPLICATION, diff --git a/src/engines/cse/interpreter.ts b/src/engines/cse/interpreter.ts index 4d67784b..7be8df98 100644 --- a/src/engines/cse/interpreter.ts +++ b/src/engines/cse/interpreter.ts @@ -14,7 +14,7 @@ import { builtIns, toPythonString } from "../../stdlib"; import { Group } from "../../stdlib/utils"; import { TokenType } from "../../tokens"; import { CSEBreak, RecursivePartial, Representation, Result } from "../../types"; -import { Closure } from "./closure"; +import { Closure, isStatementSequence } from "./closure"; import { Context } from "./context"; import { Control, ControlItem } from "./control"; import { @@ -36,13 +36,11 @@ import { BoolOpInstr, BranchInstr, EnvInstr, - Instr, InstrType, ListAccessInstr, ListAssmtInstr, ListInstr, Node, - StatementSequence, UnOpInstr, WhileInstr, } from "./types"; @@ -306,7 +304,7 @@ export async function* generateCSEMachineStateStream( } } else { // Command is an instruction - const instr = command as Instr; + const instr = command; await cmdEvaluators[instr.instrType]( code, command, @@ -843,10 +841,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { if (!top) { return; } - if ( - isNode(top) || - ((top as Instr).instrType !== InstrType.WHILE && (top as Instr).instrType !== InstrType.FOR) - ) { + if (isNode(top) || (top.instrType !== InstrType.WHILE && top.instrType !== InstrType.FOR)) { control.pop(); control.push(command); } @@ -870,10 +865,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(top); return; } - if ( - isNode(top2) || - ((top2 as Instr).instrType !== InstrType.WHILE && (top2 as Instr).instrType !== InstrType.FOR) - ) { + if (isNode(top2) || (top2.instrType !== InstrType.WHILE && top2.instrType !== InstrType.FOR)) { control.push(command); return; } @@ -1010,11 +1002,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { if (condition && !isFalsy(condition)) { control.push(instr); control.push(instr.test); - if (instr.body && "type" in instr.body && instr.body.type === "StatementSequence") { - control.push(...instr.body.body.slice().reverse()); - } else { - control.push(instr.body); - } + control.push(...instr.body.body.slice().reverse()); } }, [InstrType.APPLICATION]: async function ( @@ -1045,19 +1033,18 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { : rawArgs.flatMap((val, i) => { if (!spreadSet.has(i)) return val; if (val?.type === "list") { - return (val as { type: "list"; value: Value[] }).value; + return val.value; } handleRuntimeError( context, new error.TypeError( code, - instr.srcNode as ExprNS.Call, + instr.srcNode, context, val ? val.type : "NoneType", "iterable", ), ); - return []; // unreachable, satisfies TypeScript }); const callable = stash.pop(); @@ -1069,7 +1056,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { control.push(instrCreator.endOfFunctionBodyInstr(instr.srcNode)); } - const newEnv = createEnvironment(code, context, closure, args, instr.srcNode as ExprNS.Call); + const newEnv = createEnvironment(code, context, closure, args, instr.srcNode); pushEnvironment(context, newEnv); const closureNode = closure.node; @@ -1088,7 +1075,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { context, new error.TypeError( code, - instr.srcNode as ExprNS.Call, + instr.srcNode, context, callable ? callable.type : "NoneType", "callable", @@ -1127,7 +1114,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { code, instr.srcNode as ExprNS.Expr, context, - (index as Value).type, + index?.type || "NoneType", "int", ), ); @@ -1160,15 +1147,15 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { if (condition && !isFalsy(condition)) { const consequent = instr.consequent; - if (consequent && "type" in consequent && consequent.type === "StatementSequence") { - control.push(...(consequent as StatementSequence).body.slice().reverse()); + if (isStatementSequence(consequent)) { + control.push(...consequent.body.slice().reverse()); } else if (consequent) { control.push(consequent); } } else if (instr.alternate) { const alternate = instr.alternate; - if (alternate && "type" in alternate && alternate.type === "StatementSequence") { - control.push(...(alternate as StatementSequence).body.slice().reverse()); + if (isStatementSequence(alternate)) { + control.push(...alternate.body.slice().reverse()); } else if (alternate) { control.push(alternate); } diff --git a/src/engines/cse/operators.ts b/src/engines/cse/operators.ts index 02b6d991..676229c8 100644 --- a/src/engines/cse/operators.ts +++ b/src/engines/cse/operators.ts @@ -362,11 +362,6 @@ export function evaluateBinaryExpression( ); } - const leftNum = left.value; - const rightNum = right.value; - const leftType = left.type; - const rightType = right.type; - // Numeric Operations (number or bigint) switch (operator) { case TokenType.PLUS: @@ -379,9 +374,9 @@ export function evaluateBinaryExpression( // If either operand is a number, perform the operation with numbers (with potential loss of precision for bigints), // otherwise perform the operation using bigints if both operands are bigints. This mimics Python's behavior of coercing to float for mixed int/float operations, // while allowing for arbitrary precision with bigints. - if (leftType === "number" || rightType === "number") { - const l = Number(leftNum); - const r = Number(rightNum); + if (left.type === "number" || right.type === "number") { + const l = Number(left.value); + const r = Number(right.value); switch (operator) { case TokenType.PLUS: return { type: "number", value: l + r }; @@ -415,9 +410,9 @@ export function evaluateBinaryExpression( return { type: "number", value: l ** r }; } } - if (leftType === "bigint" && rightType === "bigint") { - const l = leftNum as bigint; - const r = rightNum as bigint; + if (left.type === "bigint" && right.type === "bigint") { + const l = left.value; + const r = right.value; switch (operator) { case TokenType.PLUS: return { type: "bigint", value: l + r }; @@ -434,7 +429,7 @@ export function evaluateBinaryExpression( if (r === 0n) { handleRuntimeError(context, new ZeroDivisionError(code, command)); } - return { type: "bigint", value: (l - (pythonMod(l, r) as bigint)) / r }; + return { type: "bigint", value: (l - (pythonMod(l, r))) / r }; case TokenType.PERCENT: if (r === 0n) { handleRuntimeError(context, new ZeroDivisionError(code, command)); diff --git a/src/engines/cse/stash.ts b/src/engines/cse/stash.ts index 0f4a0b05..be8286bf 100644 --- a/src/engines/cse/stash.ts +++ b/src/engines/cse/stash.ts @@ -1,9 +1,8 @@ // Value.ts -import { StmtNS } from "../../ast-types"; +import { ExprNS, StmtNS } from "../../ast-types"; import { PyComplexNumber } from "../../types"; import { Closure } from "./closure"; import { Context } from "./context"; -import { ControlItem } from "./control"; import { Environment } from "./environment"; import { Stack } from "./stack"; @@ -88,8 +87,8 @@ export interface BuiltinValue { type: "builtin"; name: string; func: - | ((args: Value[], code: string, command: ControlItem, context: Context) => Value) - | ((args: Value[], code: string, command: ControlItem, context: Context) => Promise); + | ((args: Value[], code: string, command: ExprNS.Expr, context: Context) => Value) + | ((args: Value[], code: string, command: ExprNS.Expr, context: Context) => Promise); minArgs: number; } diff --git a/src/engines/cse/types.ts b/src/engines/cse/types.ts index 346ddc1f..9e96159d 100644 --- a/src/engines/cse/types.ts +++ b/src/engines/cse/types.ts @@ -102,7 +102,7 @@ export interface AppInstr extends BaseInstr { instrType: InstrType.APPLICATION; numOfArgs: number; spreadIndices: number[]; - srcNode: Node; + srcNode: ExprNS.Call; } export interface BranchInstr extends BaseInstr { diff --git a/src/engines/cse/utils.ts b/src/engines/cse/utils.ts index cd3ae5f6..73a08752 100644 --- a/src/engines/cse/utils.ts +++ b/src/engines/cse/utils.ts @@ -249,7 +249,7 @@ export function isEnvDependent(item: ControlItem | null | undefined): boolean { } function isInstr(item: ControlItem): item is Instr & { isEnvDependent?: boolean } { - return (item as Instr).instrType !== undefined; + return "instrType" in item; } export const envChanging = (command: ControlItem): boolean => { @@ -310,7 +310,8 @@ export const checkStackOverFlow = (_context: Context, _control: Control) => { // return block.body.length === 1 && block.body[0].type === 'ReturnStatement' // } // } - +export function pythonMod(a: bigint, b: bigint): bigint; +export function pythonMod(a: number, b: number): number; export function pythonMod(a: number | bigint, b: number | bigint): number | bigint { if (typeof a === "bigint" || typeof b === "bigint") { const big_a = BigInt(a); diff --git a/src/engines/svml/svml-interpreter.ts b/src/engines/svml/svml-interpreter.ts index 2632d08c..e750afe2 100644 --- a/src/engines/svml/svml-interpreter.ts +++ b/src/engines/svml/svml-interpreter.ts @@ -1,7 +1,7 @@ import { pythonMod } from "../cse/utils"; +import { executePrimitive } from "./builtins"; import { UnsupportedOperandTypeError, ZeroDivisionError } from "./errors"; import OpCodes from "./opcodes"; -import { executePrimitive } from "./builtins"; import { getSVMLType, isSVMLObject, @@ -303,7 +303,7 @@ export class SVMLInterpreter { const a = left as number; const b = right as number; if (b === 0) throw new ZeroDivisionError("integer modulo by zero"); - this.push(pythonMod(a, b) as number); + this.push(pythonMod(a, b)); } else { throw new UnsupportedOperandTypeError("%", leftType, rightType); } @@ -313,7 +313,7 @@ export class SVMLInterpreter { const right = this.pop() as number; const left = this.pop() as number; if (right === 0) throw new ZeroDivisionError("integer modulo by zero"); - this.push(pythonMod(left, right) as number); + this.push(pythonMod(left, right)); break; } // Unary operations diff --git a/src/parser/lexer.ts b/src/parser/lexer.ts index b326e4b3..09c4a6cf 100644 --- a/src/parser/lexer.ts +++ b/src/parser/lexer.ts @@ -266,8 +266,8 @@ class PythonLexer implements moo.Lexer { private pos = 0; reset(data?: string, state?: moo.LexerState): this { - if (state && "pos" in state) { - this.pos = (state as PythonLexerState).pos; + if (state && "pos" in state && typeof state.pos === "number") { + this.pos = state.pos; } else if (data !== undefined) { mooLexer.reset(data); const raw: moo.Token[] = []; @@ -294,8 +294,8 @@ class PythonLexer implements moo.Lexer { return name === "indent" || name === "dedent" || mooLexer.has(name); } - formatError(token?: moo.Token, message?: string): string { - return mooLexer.formatError(token as moo.Token, message); + formatError(token: moo.Token, message: string): string { + return mooLexer.formatError(token, message); } pushState(state: string): void { @@ -319,5 +319,5 @@ class PythonLexer implements moo.Lexer { }; } } -const pythonLexer: moo.Lexer = new PythonLexer(); +const pythonLexer = new PythonLexer(); export default pythonLexer; diff --git a/src/parser/parser-adapter.ts b/src/parser/parser-adapter.ts index c36b974e..5d46725c 100644 --- a/src/parser/parser-adapter.ts +++ b/src/parser/parser-adapter.ts @@ -27,7 +27,7 @@ export class NearleyParser { const parser = new nearley.Parser( nearley.Grammar.fromCompiled({ ...(grammar as unknown as nearley.CompiledRules), - Lexer: pythonLexer as nearley.Lexer, + Lexer: pythonLexer, }), ); diff --git a/src/resolver/resolver.ts b/src/resolver/resolver.ts index 39f99a85..b3ff24c9 100644 --- a/src/resolver/resolver.ts +++ b/src/resolver/resolver.ts @@ -195,19 +195,19 @@ export class Resolver implements StmtNS.Visitor, ExprNS.Visitor { new Map([ // misc library ...constants.builtInFuncs.map( - (name: string) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token], + (name: string) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as const, ), ["range", new Token(TokenType.NAME, "range", 0, 0, 0)], ...constants.constants.map( - (name: string) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token], + (name: string) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as const, ), ...groups.flatMap(group => Array.from(group.builtins.entries()).map( - ([name]) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token], + ([name]) => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as const, ), ), ...preludeNames.map( - name => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as [string, Token], + name => [name, new Token(TokenType.NAME, name, 0, 0, 0)] as const, ), ]), ); diff --git a/src/stdlib.ts b/src/stdlib.ts index 1d03babd..b6fe4646 100644 --- a/src/stdlib.ts +++ b/src/stdlib.ts @@ -34,7 +34,7 @@ export function Validate>( _target: unknown, _propertyKey: string, descriptor: TypedPropertyDescriptor< - (args: Value[], source: string, command: ControlItem, context: Context) => T + (args: Value[], source: string, command: ExprNS.Call, context: Context) => T >, ): void { const originalMethod = descriptor.value!; @@ -42,7 +42,7 @@ export function Validate>( descriptor.value = function ( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): T { if (minArgs !== null && args.length < minArgs) { @@ -50,7 +50,7 @@ export function Validate>( context, new MissingRequiredPositionalError( source, - command as ExprNS.Expr, + command, functionName, minArgs, args, @@ -64,7 +64,7 @@ export function Validate>( context, new TooManyPositionalArgumentsError( source, - command as ExprNS.Expr, + command, functionName, maxArgs, args, @@ -80,12 +80,12 @@ export function Validate>( export class BuiltInFunctions { @Validate(1, 1, "arity", true) - static arity(args: Value[], source: string, command: ControlItem, context: Context): BigIntValue { + static arity(args: Value[], source: string, command: ExprNS.Call, context: Context): BigIntValue { const func = args[0]; if (func.type !== "builtin" && func.type !== "closure") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, func.type, "function"), + new TypeError(source, command, context, func.type, "function"), ); } if (func.type === "closure") { @@ -99,7 +99,7 @@ export class BuiltInFunctions { } @Validate(null, 2, "int", true) - static int(args: Value[], source: string, command: ControlItem, context: Context): BigIntValue { + static int(args: Value[], source: string, command: ExprNS.Call, context: Context): BigIntValue { if (args.length === 0) { return { type: "bigint", value: BigInt(0) }; } @@ -107,7 +107,7 @@ export class BuiltInFunctions { if (!isNumeric(arg) && arg.type !== "string" && arg.type !== "bool") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, arg.type, "str, int, float or bool"), + new TypeError(source, command, context, arg.type, "str, int, float or bool"), ); } @@ -124,7 +124,7 @@ export class BuiltInFunctions { if (!/^[+-]?\d+$/.test(str)) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "int"), + new ValueError(source, command, context, "int"), ); } return { type: "bigint", value: BigInt(str) }; @@ -135,13 +135,13 @@ export class BuiltInFunctions { if (arg.type !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, arg.type, "string"), + new TypeError(source, command, context, arg.type, "string"), ); } if (baseArg.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, baseArg.type, "int"), + new TypeError(source, command, context, baseArg.type, "int"), ); } @@ -169,13 +169,13 @@ export class BuiltInFunctions { } if (base < 2 || base > 36) { - handleRuntimeError(context, new ValueError(source, command as ExprNS.Expr, context, "int")); + handleRuntimeError(context, new ValueError(source, command, context, "int")); } const validChars = "0123456789abcdefghijklmnopqrstuvwxyz".substring(0, base); const regex = new RegExp(`^[${validChars}]+$`, "i"); if (!regex.test(str)) { - handleRuntimeError(context, new ValueError(source, command as ExprNS.Expr, context, "int")); + handleRuntimeError(context, new ValueError(source, command, context, "int")); } let res = BigInt(0); @@ -186,7 +186,7 @@ export class BuiltInFunctions { } @Validate(null, 1, "float", true) - static float(args: Value[], source: string, command: ControlItem, context: Context): NumberValue { + static float(args: Value[], source: string, command: ExprNS.Call, context: Context): NumberValue { if (args.length === 0) { return { type: "number", value: 0 }; } @@ -217,7 +217,7 @@ export class BuiltInFunctions { if (isNaN(num)) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "float"), + new ValueError(source, command, context, "float"), ); } return { type: "number", value: num }; @@ -226,7 +226,7 @@ export class BuiltInFunctions { context, new TypeError( source, - command as ExprNS.Expr, + command, context, val.type, "'float', 'int', 'bool' or 'str'", @@ -238,7 +238,7 @@ export class BuiltInFunctions { static complex( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): ComplexValue { if (args.length === 0) { @@ -255,12 +255,12 @@ export class BuiltInFunctions { ) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, val.type, "complex"), + new TypeError(source, command, context, val.type, "complex"), ); } return { type: "complex", - value: PyComplexNumber.fromValue(context, source, command as ExprNS.Expr, val.value), + value: PyComplexNumber.fromValue(context, source, command, val.value), }; } const invalidType = args.filter( @@ -275,7 +275,7 @@ export class BuiltInFunctions { context, new TypeError( source, - command as ExprNS.Expr, + command, context, invalidType[0].type, "'int', 'float', 'bool' or 'complex'", @@ -283,30 +283,30 @@ export class BuiltInFunctions { ); } const [real, imag] = args as (BigIntValue | NumberValue | BoolValue | ComplexValue)[]; - const realPart = PyComplexNumber.fromValue(context, source, command as ExprNS.Expr, real.value); - const imagPart = PyComplexNumber.fromValue(context, source, command as ExprNS.Expr, imag.value); + const realPart = PyComplexNumber.fromValue(context, source, command, real.value); + const imagPart = PyComplexNumber.fromValue(context, source, command, imag.value); return { type: "complex", value: realPart.add(imagPart.mul(new PyComplexNumber(0, 1))) }; } @Validate(1, 1, "real", true) - static real(args: Value[], source: string, command: ControlItem, context: Context): NumberValue { + static real(args: Value[], source: string, command: ExprNS.Call, context: Context): NumberValue { const val = args[0]; if (val.type !== "complex") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, val.type, "complex"), + new TypeError(source, command, context, val.type, "complex"), ); } return { type: "number", value: val.value.real }; } @Validate(1, 1, "imag", true) - static imag(args: Value[], source: string, command: ControlItem, context: Context): NumberValue { + static imag(args: Value[], source: string, command: ExprNS.Call, context: Context): NumberValue { const val = args[0]; if (val.type !== "complex") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, val.type, "complex"), + new TypeError(source, command, context, val.type, "complex"), ); } return { type: "number", value: val.value.imag }; @@ -325,7 +325,7 @@ export class BuiltInFunctions { static abs( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue | NumberValue { const x = args[0]; @@ -350,7 +350,7 @@ export class BuiltInFunctions { context, new TypeError( source, - command as ExprNS.Expr, + command, context, args[0].type, "float', 'int' or 'complex", @@ -360,7 +360,7 @@ export class BuiltInFunctions { } @Validate(1, 1, "len", true) - static len(args: Value[], source: string, command: ControlItem, context: Context): BigIntValue { + static len(args: Value[], source: string, command: ExprNS.Call, context: Context): BigIntValue { const val = args[0]; if (val.type === "string" || val.type === "list") { // The spread operator is used to count the number of Unicode code points @@ -369,27 +369,27 @@ export class BuiltInFunctions { } handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, val.type, "object with length"), + new TypeError(source, command, context, val.type, "object with length"), ); } - static error(args: Value[], _source: string, command: ControlItem, context: Context): Value { + static error(args: Value[], _source: string, command: ExprNS.Call, context: Context): Value { const output = "Error: " + args.map(arg => toPythonString(arg)).join(" ") + "\n"; - handleRuntimeError(context, new UserError(output, command as ExprNS.Expr)); + handleRuntimeError(context, new UserError(output, command)); } @Validate(1, 1, "math_acos", false) static math_acos( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -403,7 +403,7 @@ export class BuiltInFunctions { if (num < -1 || num > 1) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_acos"), + new ValueError(source, command, context, "math_acos"), ); } @@ -415,7 +415,7 @@ export class BuiltInFunctions { static math_acosh( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; @@ -423,7 +423,7 @@ export class BuiltInFunctions { if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -437,7 +437,7 @@ export class BuiltInFunctions { if (num < 1) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_acosh"), + new ValueError(source, command, context, "math_acosh"), ); } @@ -449,14 +449,14 @@ export class BuiltInFunctions { static math_asin( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -470,7 +470,7 @@ export class BuiltInFunctions { if (num < -1 || num > 1) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_asin"), + new ValueError(source, command, context, "math_asin"), ); } @@ -482,14 +482,14 @@ export class BuiltInFunctions { static math_asinh( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -508,14 +508,14 @@ export class BuiltInFunctions { static math_atan( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -534,7 +534,7 @@ export class BuiltInFunctions { static math_atan2( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const y = args[0]; @@ -542,12 +542,12 @@ export class BuiltInFunctions { if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } else if (!isNumeric(y)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, y.type, "float' or 'int"), + new TypeError(source, command, context, y.type, "float' or 'int"), ); } @@ -572,14 +572,14 @@ export class BuiltInFunctions { static math_atanh( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -593,7 +593,7 @@ export class BuiltInFunctions { if (num <= -1 || num >= 1) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_atanh"), + new ValueError(source, command, context, "math_atanh"), ); } @@ -605,14 +605,14 @@ export class BuiltInFunctions { static math_cos( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -631,14 +631,14 @@ export class BuiltInFunctions { static math_cosh( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -657,14 +657,14 @@ export class BuiltInFunctions { static math_degrees( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -683,14 +683,14 @@ export class BuiltInFunctions { static math_erf( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -710,14 +710,14 @@ export class BuiltInFunctions { static math_erfc( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -730,7 +730,7 @@ export class BuiltInFunctions { static math_comb( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue { const n = args[0]; @@ -739,12 +739,12 @@ export class BuiltInFunctions { if (n.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, n.type, "int"), + new TypeError(source, command, context, n.type, "int"), ); } else if (k.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, k.type, "int"), + new TypeError(source, command, context, k.type, "int"), ); } @@ -754,7 +754,7 @@ export class BuiltInFunctions { if (nVal < 0 || kVal < 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_comb"), + new ValueError(source, command, context, "math_comb"), ); } @@ -776,7 +776,7 @@ export class BuiltInFunctions { static math_factorial( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue { const n = args[0]; @@ -784,7 +784,7 @@ export class BuiltInFunctions { if (n.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, n.type, "int"), + new TypeError(source, command, context, n.type, "int"), ); } @@ -793,7 +793,7 @@ export class BuiltInFunctions { if (nVal < 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_factorial"), + new ValueError(source, command, context, "math_factorial"), ); } @@ -813,7 +813,7 @@ export class BuiltInFunctions { static math_gcd( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue { if (args.length === 0) { @@ -824,7 +824,7 @@ export class BuiltInFunctions { if (v.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, v.type, "int"), + new TypeError(source, command, context, v.type, "int"), ); } return BigInt(v.value); @@ -861,14 +861,14 @@ export class BuiltInFunctions { static math_isqrt( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue { const nValObj = args[0]; if (nValObj.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, nValObj.type, "int"), + new TypeError(source, command, context, nValObj.type, "int"), ); } @@ -877,7 +877,7 @@ export class BuiltInFunctions { if (n < 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_isqrt"), + new ValueError(source, command, context, "math_isqrt"), ); } @@ -904,7 +904,7 @@ export class BuiltInFunctions { static math_lcm( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue { if (args.length === 0) { @@ -915,7 +915,7 @@ export class BuiltInFunctions { if (val.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, val.type, "int"), + new TypeError(source, command, context, val.type, "int"), ); } return BigInt(val.value); @@ -949,14 +949,14 @@ export class BuiltInFunctions { static math_perm( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue { const nValObj = args[0]; if (nValObj.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, nValObj.type, "int"), + new TypeError(source, command, context, nValObj.type, "int"), ); } const n = BigInt(nValObj.value); @@ -971,7 +971,7 @@ export class BuiltInFunctions { } else { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, kValObj.type, "int' or 'None"), + new TypeError(source, command, context, kValObj.type, "int' or 'None"), ); } } @@ -979,7 +979,7 @@ export class BuiltInFunctions { if (n < 0 || k < 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_perm"), + new ValueError(source, command, context, "math_perm"), ); } @@ -999,7 +999,7 @@ export class BuiltInFunctions { static math_ceil( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue { const x = args[0]; @@ -1016,7 +1016,7 @@ export class BuiltInFunctions { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1024,7 +1024,7 @@ export class BuiltInFunctions { static math_fabs( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; @@ -1040,7 +1040,7 @@ export class BuiltInFunctions { if (typeof numVal !== "number") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } const absVal: number = Math.abs(numVal); @@ -1049,7 +1049,7 @@ export class BuiltInFunctions { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1057,7 +1057,7 @@ export class BuiltInFunctions { static math_floor( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BigIntValue { const x = args[0]; @@ -1071,7 +1071,7 @@ export class BuiltInFunctions { if (typeof numVal !== "number") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } const floored: bigint = BigInt(Math.floor(numVal)); @@ -1080,7 +1080,7 @@ export class BuiltInFunctions { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1112,7 +1112,7 @@ export class BuiltInFunctions { return result; } - static toNumber(val: Value, source: string, command: ControlItem, context: Context): number { + static toNumber(val: Value, source: string, command: ExprNS.Call, context: Context): number { if (val.type === "bigint") { return Number(val.value); } else if (val.type === "number") { @@ -1120,7 +1120,7 @@ export class BuiltInFunctions { } else { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, val.type, "float' or 'int"), + new TypeError(source, command, context, val.type, "float' or 'int"), ); } } @@ -1129,7 +1129,7 @@ export class BuiltInFunctions { static math_fma( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const xVal = BuiltInFunctions.toNumber(args[0], source, command, context); @@ -1153,7 +1153,7 @@ export class BuiltInFunctions { } @Validate(2, 2, "math_fmod", false) - static math_fmod(args: Value[], source: string, command: ControlItem, context: Context): Value { + static math_fmod(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { // Convert inputs to numbers const xVal = BuiltInFunctions.toNumber(args[0], source, command, context); const yVal = BuiltInFunctions.toNumber(args[1], source, command, context); @@ -1162,7 +1162,7 @@ export class BuiltInFunctions { if (yVal === 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_fmod"), + new ValueError(source, command, context, "math_fmod"), ); } @@ -1194,7 +1194,7 @@ export class BuiltInFunctions { static math_remainder( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; @@ -1208,7 +1208,7 @@ export class BuiltInFunctions { } else { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1220,14 +1220,14 @@ export class BuiltInFunctions { } else { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, y.type, "float' or 'int"), + new TypeError(source, command, context, y.type, "float' or 'int"), ); } if (yValue === 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_remainder"), + new ValueError(source, command, context, "math_remainder"), ); } @@ -1239,7 +1239,7 @@ export class BuiltInFunctions { } @Validate(1, 1, "math_trunc", false) - static math_trunc(args: Value[], source: string, command: ControlItem, context: Context): Value { + static math_trunc(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { const x = args[0]; if (x.type === "bigint") { @@ -1251,7 +1251,7 @@ export class BuiltInFunctions { if (typeof numVal !== "number") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } let truncated: number; @@ -1267,7 +1267,7 @@ export class BuiltInFunctions { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1275,7 +1275,7 @@ export class BuiltInFunctions { static math_copysign( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const [x, y] = args; @@ -1283,12 +1283,12 @@ export class BuiltInFunctions { if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } else if (!isNumeric(y)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, y.type, "float' or 'int"), + new TypeError(source, command, context, y.type, "float' or 'int"), ); } @@ -1306,14 +1306,14 @@ export class BuiltInFunctions { static math_isfinite( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BoolValue { const xValObj = args[0]; if (!isNumeric(xValObj)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, xValObj.type, "float' or 'int"), + new TypeError(source, command, context, xValObj.type, "float' or 'int"), ); } @@ -1327,14 +1327,14 @@ export class BuiltInFunctions { static math_isinf( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BoolValue { const xValObj = args[0]; if (!isNumeric(xValObj)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, xValObj.type, "float' or 'int"), + new TypeError(source, command, context, xValObj.type, "float' or 'int"), ); } @@ -1348,14 +1348,14 @@ export class BuiltInFunctions { static math_isnan( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): BoolValue { const xValObj = args[0]; if (!isNumeric(xValObj)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, xValObj.type, "float' or 'int"), + new TypeError(source, command, context, xValObj.type, "float' or 'int"), ); } @@ -1369,7 +1369,7 @@ export class BuiltInFunctions { static math_ldexp( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const xVal = BuiltInFunctions.toNumber(args[0], source, command, context); @@ -1377,7 +1377,7 @@ export class BuiltInFunctions { if (args[1].type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[1].type, "int"), + new TypeError(source, command, context, args[1].type, "int"), ); } const expVal = args[1].value; @@ -1394,7 +1394,7 @@ export class BuiltInFunctions { static math_nextafter( _args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): Value { // TODO: Implement math_nextafter using proper bit-level manipulation and handling special cases (NaN, Infinity, steps, etc.) @@ -1405,7 +1405,7 @@ export class BuiltInFunctions { static math_ulp( _args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): Value { // TODO: Implement math_ulp to return the unit in the last place (ULP) of the given floating-point number. @@ -1416,7 +1416,7 @@ export class BuiltInFunctions { static math_cbrt( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const xVal = args[0]; @@ -1428,7 +1428,7 @@ export class BuiltInFunctions { } else { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, xVal.type, "float' or 'int"), + new TypeError(source, command, context, xVal.type, "float' or 'int"), ); } } else { @@ -1444,7 +1444,7 @@ export class BuiltInFunctions { static math_exp( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const xVal = args[0]; @@ -1456,7 +1456,7 @@ export class BuiltInFunctions { } else { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, xVal.type, "float' or 'int"), + new TypeError(source, command, context, xVal.type, "float' or 'int"), ); } } else { @@ -1471,7 +1471,7 @@ export class BuiltInFunctions { static math_exp2( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const xVal = args[0]; @@ -1483,7 +1483,7 @@ export class BuiltInFunctions { } else { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, xVal.type, "float' or 'int"), + new TypeError(source, command, context, xVal.type, "float' or 'int"), ); } } else { @@ -1498,14 +1498,14 @@ export class BuiltInFunctions { static math_expm1( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1524,14 +1524,14 @@ export class BuiltInFunctions { static math_gamma( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1545,14 +1545,14 @@ export class BuiltInFunctions { static math_lgamma( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1566,14 +1566,14 @@ export class BuiltInFunctions { static math_log( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } let num: number; @@ -1586,7 +1586,7 @@ export class BuiltInFunctions { if (num <= 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_log"), + new ValueError(source, command, context, "math_log"), ); } @@ -1598,7 +1598,7 @@ export class BuiltInFunctions { if (!isNumeric(baseArg)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, baseArg.type, "float' or 'int"), + new TypeError(source, command, context, baseArg.type, "float' or 'int"), ); } let baseNum: number; @@ -1610,7 +1610,7 @@ export class BuiltInFunctions { if (baseNum <= 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_log"), + new ValueError(source, command, context, "math_log"), ); } @@ -1622,14 +1622,14 @@ export class BuiltInFunctions { static math_log10( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "float' or 'int"), + new TypeError(source, command, context, args[0].type, "float' or 'int"), ); } let num: number; @@ -1641,7 +1641,7 @@ export class BuiltInFunctions { if (num <= 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_log10"), + new ValueError(source, command, context, "math_log10"), ); } @@ -1653,14 +1653,14 @@ export class BuiltInFunctions { static math_log1p( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "float' or 'int"), + new TypeError(source, command, context, args[0].type, "float' or 'int"), ); } let num: number; @@ -1672,7 +1672,7 @@ export class BuiltInFunctions { if (1 + num <= 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_log1p"), + new ValueError(source, command, context, "math_log1p"), ); } @@ -1684,14 +1684,14 @@ export class BuiltInFunctions { static math_log2( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "float' or 'int"), + new TypeError(source, command, context, args[0].type, "float' or 'int"), ); } let num: number; @@ -1703,7 +1703,7 @@ export class BuiltInFunctions { if (num <= 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_log2"), + new ValueError(source, command, context, "math_log2"), ); } @@ -1715,7 +1715,7 @@ export class BuiltInFunctions { static math_pow( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const base = args[0]; @@ -1724,12 +1724,12 @@ export class BuiltInFunctions { if (!isNumeric(base)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, base.type, "float' or 'int"), + new TypeError(source, command, context, base.type, "float' or 'int"), ); } else if (!isNumeric(exp)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, exp.type, "float' or 'int"), + new TypeError(source, command, context, exp.type, "float' or 'int"), ); } @@ -1755,14 +1755,14 @@ export class BuiltInFunctions { static math_radians( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1778,12 +1778,12 @@ export class BuiltInFunctions { } @Validate(1, 1, "math_sin", false) - static math_sin(args: Value[], source: string, command: ControlItem, context: Context): Value { + static math_sin(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1799,12 +1799,12 @@ export class BuiltInFunctions { } @Validate(1, 1, "math_sinh", false) - static math_sinh(args: Value[], source: string, command: ControlItem, context: Context): Value { + static math_sinh(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1820,12 +1820,12 @@ export class BuiltInFunctions { } @Validate(1, 1, "math_tan", false) - static math_tan(args: Value[], source: string, command: ControlItem, context: Context): Value { + static math_tan(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1844,14 +1844,14 @@ export class BuiltInFunctions { static math_tanh( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1870,14 +1870,14 @@ export class BuiltInFunctions { static math_sqrt( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue { const x = args[0]; if (!isNumeric(x)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, x.type, "float' or 'int"), + new TypeError(source, command, context, x.type, "float' or 'int"), ); } @@ -1891,7 +1891,7 @@ export class BuiltInFunctions { if (num < 0) { handleRuntimeError( context, - new ValueError(source, command as ExprNS.Expr, context, "math_sqrt"), + new ValueError(source, command, context, "math_sqrt"), ); } @@ -1900,7 +1900,7 @@ export class BuiltInFunctions { } @Validate(2, null, "max", true) - static max(args: Value[], source: string, command: ControlItem, context: Context): Value { + static max(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { const numericTypes = ["bigint", "number"]; const firstType = args[0].type; const isNumericValue = numericTypes.includes(firstType); @@ -1911,13 +1911,13 @@ export class BuiltInFunctions { if (isNumericValue && !numericTypes.includes(t)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[i].type, "float' or 'int"), + new TypeError(source, command, context, args[i].type, "float' or 'int"), ); } if (isString && t !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[i].type, "string"), + new TypeError(source, command, context, args[i].type, "string"), ); } } @@ -1938,7 +1938,7 @@ export class BuiltInFunctions { if (args[0].type !== "number" && args[0].type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "float' or 'int"), + new TypeError(source, command, context, args[0].type, "float' or 'int"), ); } let maxVal: number = Number(args[0].value); @@ -1947,7 +1947,7 @@ export class BuiltInFunctions { if (!isNumeric(arg)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, arg.type, "float' or 'int"), + new TypeError(source, command, context, arg.type, "float' or 'int"), ); } const curr: number = Number(arg.value); @@ -1960,7 +1960,7 @@ export class BuiltInFunctions { if (args[0].type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "int"), + new TypeError(source, command, context, args[0].type, "int"), ); } let maxVal: bigint = args[0].value; @@ -1969,7 +1969,7 @@ export class BuiltInFunctions { if (arg.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, arg.type, "int"), + new TypeError(source, command, context, arg.type, "int"), ); } const curr: bigint = arg.value; @@ -1983,7 +1983,7 @@ export class BuiltInFunctions { if (args[0].type !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "string"), + new TypeError(source, command, context, args[0].type, "string"), ); } let maxVal = args[0].value; @@ -1992,7 +1992,7 @@ export class BuiltInFunctions { if (arg.type !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, arg.type, "string"), + new TypeError(source, command, context, arg.type, "string"), ); } const curr = arg.value; @@ -2010,13 +2010,13 @@ export class BuiltInFunctions { } @Validate(2, null, "min", true) - static min(args: Value[], source: string, command: ControlItem, context: Context): Value { + static min(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { if (args.length < 2) { handleRuntimeError( context, new MissingRequiredPositionalError( source, - command as ExprNS.Expr, + command, "min", Number(2), args, @@ -2035,13 +2035,13 @@ export class BuiltInFunctions { if (isNumericValue && !numericTypes.includes(t)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[i].type, "float' or 'int"), + new TypeError(source, command, context, args[i].type, "float' or 'int"), ); } if (isString && t !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[i].type, "string"), + new TypeError(source, command, context, args[i].type, "string"), ); } } @@ -2062,7 +2062,7 @@ export class BuiltInFunctions { if (args[0].type !== "number" && args[0].type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "float' or 'int"), + new TypeError(source, command, context, args[0].type, "float' or 'int"), ); } let maxVal: number = Number(args[0].value); @@ -2071,7 +2071,7 @@ export class BuiltInFunctions { if (!isNumeric(arg)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, arg.type, "float' or 'int"), + new TypeError(source, command, context, arg.type, "float' or 'int"), ); } const curr: number = Number(arg.value); @@ -2084,7 +2084,7 @@ export class BuiltInFunctions { if (args[0].type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "int"), + new TypeError(source, command, context, args[0].type, "int"), ); } let maxVal: bigint = args[0].value; @@ -2093,7 +2093,7 @@ export class BuiltInFunctions { if (arg.type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, arg.type, "int"), + new TypeError(source, command, context, arg.type, "int"), ); } const curr: bigint = arg.value; @@ -2107,7 +2107,7 @@ export class BuiltInFunctions { if (args[0].type !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "string"), + new TypeError(source, command, context, args[0].type, "string"), ); } let maxVal = args[0].value; @@ -2116,7 +2116,7 @@ export class BuiltInFunctions { if (arg.type !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, arg.type, "string"), + new TypeError(source, command, context, arg.type, "string"), ); } const curr = arg.value; @@ -2137,7 +2137,7 @@ export class BuiltInFunctions { static random_random( _args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): NumberValue { const result = Math.random(); @@ -2148,14 +2148,14 @@ export class BuiltInFunctions { static round( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): NumberValue | BigIntValue { const numArg = args[0]; if (!isNumeric(numArg)) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, numArg.type, "float' or 'int"), + new TypeError(source, command, context, numArg.type, "float' or 'int"), ); } @@ -2164,7 +2164,7 @@ export class BuiltInFunctions { if (args[1].type !== "bigint") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[1].type, "int"), + new TypeError(source, command, context, args[1].type, "int"), ); } ndigitsArg = args[1]; @@ -2214,7 +2214,7 @@ export class BuiltInFunctions { static time_time( _args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): NumberValue { const currentTime = Date.now(); @@ -2225,7 +2225,7 @@ export class BuiltInFunctions { static is_none( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { const obj = args[0]; @@ -2236,7 +2236,7 @@ export class BuiltInFunctions { static is_float( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { const obj = args[0]; @@ -2247,7 +2247,7 @@ export class BuiltInFunctions { static is_string( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { const obj = args[0]; @@ -2258,7 +2258,7 @@ export class BuiltInFunctions { static is_boolean( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { const obj = args[0]; @@ -2269,7 +2269,7 @@ export class BuiltInFunctions { static is_complex( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { const obj = args[0]; @@ -2280,7 +2280,7 @@ export class BuiltInFunctions { static is_int( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { const obj = args[0]; @@ -2291,7 +2291,7 @@ export class BuiltInFunctions { static is_function( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { const obj = args[0]; @@ -2304,7 +2304,7 @@ export class BuiltInFunctions { static async input( _args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, context: Context, ): Promise { const userInput = await receiveInput(context); @@ -2314,7 +2314,7 @@ export class BuiltInFunctions { static async print( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, context: Context, ): Promise { const output = args.map(arg => toPythonString(arg)).join(" "); @@ -2324,7 +2324,7 @@ export class BuiltInFunctions { static str( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): StringValue { if (args.length === 0) { @@ -2338,7 +2338,7 @@ export class BuiltInFunctions { static repr( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): StringValue { const obj = args[0]; diff --git a/src/stdlib/linked-list.ts b/src/stdlib/linked-list.ts index 1d203830..dac5bdc3 100644 --- a/src/stdlib/linked-list.ts +++ b/src/stdlib/linked-list.ts @@ -14,10 +14,14 @@ import { displayOutput } from "../engines/cse/streams"; import { TypeError } from "../errors"; import { minArgMap, toPythonString, Validate } from "../stdlib"; import linkedListPrelude from "./linked-list.prelude"; -import { Group, GroupName } from "./utils"; +import { GroupName } from "./utils"; const linkedListBuiltins = new Map(); +const isPair = (value: Value): value is ListValue => { + return value.type === "list" && value.value.length === 2; +}; + class LinkedListBuiltins { @Validate(2, 2, "pair", true) static pair(args: Value[], _source: string, _command: ControlItem, _context: Context): ListValue { @@ -28,7 +32,7 @@ class LinkedListBuiltins { static linked_list( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): ListValue | NoneValue { if (args.length === 0) { @@ -43,29 +47,29 @@ class LinkedListBuiltins { static is_pair( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { - return { type: "bool", value: args[0].type === "list" && args[0].value.length === 2 }; + return { type: "bool", value: isPair(args[0]) }; } @Validate(1, 1, "head", true) - static head(args: Value[], source: string, command: ControlItem, context: Context): Value { - if (args[0].type !== "list" || args[0].value.length !== 2) { + static head(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { + if (!isPair(args[0])) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "pair"), + new TypeError(source, command, context, args[0].type, "pair"), ); } return args[0].value[0]; } @Validate(1, 1, "tail", true) - static tail(args: Value[], source: string, command: ControlItem, context: Context): Value { - if (args[0].type !== "list" || args[0].value.length !== 2) { + static tail(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { + if (!isPair(args[0])) { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "pair"), + new TypeError(source, command, context, args[0].type, "pair"), ); } return args[0].value[1]; @@ -76,8 +80,7 @@ class LinkedListBuiltins { return true; } return ( - value.type === "list" && - value.value.length === 2 && + isPair(value) && LinkedListBuiltins._is_linked_list(value.value[1]) ); } @@ -85,22 +88,21 @@ class LinkedListBuiltins { static _print_linked_list( value: Value, source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): StringValue { if (!LinkedListBuiltins._is_linked_list(value)) { - const isPairResult = LinkedListBuiltins.is_pair([value], source, command, context); - if (!isPairResult.value) { + if (!isPair(value)) { return { type: "string", value: toPythonString(value) }; } const string1 = LinkedListBuiltins._print_linked_list( - (value as ListValue).value[0], + value.value[0], source, command, context, ); const string2 = LinkedListBuiltins._print_linked_list( - (value as ListValue).value[1], + value.value[1], source, command, context, @@ -131,7 +133,7 @@ class LinkedListBuiltins { static async print_linked_list( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): Promise { const stringValue = LinkedListBuiltins._print_linked_list(args[0], source, command, context); @@ -156,4 +158,4 @@ export default { name: GroupName.LINKED_LISTS, prelude: linkedListPrelude, builtins: linkedListBuiltins, -} as Group; +}; diff --git a/src/stdlib/list.ts b/src/stdlib/list.ts index 2dd5c0d1..e97695ad 100644 --- a/src/stdlib/list.ts +++ b/src/stdlib/list.ts @@ -1,9 +1,9 @@ +import { ExprNS } from "../ast-types"; import { Context } from "../engines/cse/context"; -import { ControlItem } from "../engines/cse/control"; import { BigIntValue, BoolValue, BuiltinValue, Value } from "../engines/cse/stash"; import { minArgMap, Validate } from "../stdlib"; import listPrelude from "./list.prelude"; -import { Group, GroupName } from "./utils"; +import { GroupName } from "./utils"; const listBuiltins = new Map(); @@ -12,7 +12,7 @@ class ListBuiltins { static list_length( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BigIntValue { const list = args[0]; @@ -26,7 +26,7 @@ class ListBuiltins { static is_list( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): BoolValue { const list = args[0]; @@ -38,7 +38,7 @@ class ListBuiltins { static _gen_list( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): Value { const length = args[0]; @@ -69,4 +69,4 @@ export default { name: GroupName.LIST, prelude: listPrelude, builtins: listBuiltins, -} as Group; +}; diff --git a/src/stdlib/pairmutator.ts b/src/stdlib/pairmutator.ts index f6a4a9af..f55568aa 100644 --- a/src/stdlib/pairmutator.ts +++ b/src/stdlib/pairmutator.ts @@ -1,8 +1,8 @@ +import { ExprNS } from "../ast-types"; import { Context } from "../engines/cse/context"; -import { ControlItem } from "../engines/cse/control"; import { BuiltinValue, NoneValue, Value } from "../engines/cse/stash"; import { minArgMap, Validate } from "../stdlib"; -import { Group, GroupName } from "./utils"; +import { GroupName } from "./utils"; const pairmutatorBuiltins = new Map(); @@ -11,7 +11,7 @@ class PairmutatorBuiltins { static set_head( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): NoneValue { const head = args[0]; @@ -27,7 +27,7 @@ class PairmutatorBuiltins { static set_tail( args: Value[], _source: string, - _command: ControlItem, + _command: ExprNS.Call, _context: Context, ): NoneValue { const head = args[0]; @@ -58,4 +58,4 @@ export default { name: GroupName.PAIRMUTATORS, prelude: ``, builtins: pairmutatorBuiltins, -} as Group; +}; diff --git a/src/stdlib/parser.ts b/src/stdlib/parser.ts index 3b8d3d7c..a8fde4fe 100644 --- a/src/stdlib/parser.ts +++ b/src/stdlib/parser.ts @@ -1,24 +1,19 @@ -import { parse } from "../parser"; -import pythonLexer from "../parser/lexer"; import { ExprNS, FunctionParam, StmtNS } from "../ast-types"; import { Context } from "../engines/cse/context"; +import { handleRuntimeError } from "../engines/cse/error"; import { - Value, BuiltinValue, ListValue, NoneValue, StringValue, - BoolValue, - BigIntValue, - NumberValue, - ComplexValue, + Value } from "../engines/cse/stash"; -import { handleRuntimeError } from "../engines/cse/error"; +import { operatorTranslator } from "../engines/cse/types"; import { TypeError } from "../errors/errors"; -import { ControlItem } from "../engines/cse/control"; +import { parse } from "../parser"; +import pythonLexer from "../parser/lexer"; import { minArgMap, Validate } from "../stdlib"; -import { Group, GroupName } from "./utils"; -import { operatorTranslator } from "../engines/cse/types"; +import { GroupName } from "./utils"; const None: NoneValue = { type: "none" }; @@ -57,7 +52,7 @@ function makeSequenceIfNeeded( return transform(statements[0], declaredNames); } return vector_to_linked_list([ - { type: "string", value: "sequence" } as StringValue, + { type: "string", value: "sequence" }, vector_to_linked_list(statements.map(stmt => transform(stmt, declaredNames))), ]); } @@ -65,7 +60,7 @@ function makeSequenceIfNeeded( function makeBlockIfNeeded(statements: StmtNS.Stmt[], declaredNames: Set): Value { return hasDeclaration(statements) ? vector_to_linked_list([ - { type: "string", value: "block" } as StringValue, + { type: "string", value: "block" }, makeSequenceIfNeeded(statements, declaredNames), ]) : makeSequenceIfNeeded(statements, declaredNames); @@ -73,12 +68,12 @@ function makeBlockIfNeeded(statements: StmtNS.Stmt[], declaredNames: Set function transformParam(p: FunctionParam): Value { const nameNode = vector_to_linked_list([ - { type: "string", value: "name" } as StringValue, - { type: "string", value: p.lexeme } as StringValue, + { type: "string", value: "name" }, + { type: "string", value: p.lexeme }, ]); if (p.isStarred) { return vector_to_linked_list([ - { type: "string", value: "rest_element" } as StringValue, + { type: "string", value: "rest_element" }, nameNode, ]); } @@ -99,68 +94,68 @@ function transform( case "Literal": { const lit = node as ExprNS.Literal; let val: Value; - if (typeof lit.value === "number") val = { type: "number", value: lit.value } as NumberValue; + if (typeof lit.value === "number") val = { type: "number", value: lit.value }; else if (typeof lit.value === "boolean") - val = { type: "bool", value: lit.value } as BoolValue; + val = { type: "bool", value: lit.value }; else if (typeof lit.value === "string") - val = { type: "string", value: lit.value } as StringValue; + val = { type: "string", value: lit.value }; else val = None; - return vector_to_linked_list([{ type: "string", value: "literal" } as StringValue, val]); + return vector_to_linked_list([{ type: "string", value: "literal" }, val]); } case "BigIntLiteral": return vector_to_linked_list([ - { type: "string", value: "literal" } as StringValue, - { type: "bigint", value: BigInt((node as ExprNS.BigIntLiteral).value) } as BigIntValue, + { type: "string", value: "literal" }, + { type: "bigint", value: BigInt((node as ExprNS.BigIntLiteral).value) }, ]); case "Complex": return vector_to_linked_list([ - { type: "string", value: "literal" } as StringValue, - { type: "complex", value: (node as ExprNS.Complex).value } as ComplexValue, + { type: "string", value: "literal" }, + { type: "complex", value: (node as ExprNS.Complex).value }, ]); case "None": - return vector_to_linked_list([{ type: "string", value: "literal" } as StringValue, None]); + return vector_to_linked_list([{ type: "string", value: "literal" }, None]); case "Variable": return vector_to_linked_list([ - { type: "string", value: "name" } as StringValue, - { type: "string", value: (node as ExprNS.Variable).name.lexeme } as StringValue, + { type: "string", value: "name" }, + { type: "string", value: (node as ExprNS.Variable).name.lexeme }, ]); case "Binary": return vector_to_linked_list([ - { type: "string", value: "binary_operator_combination" } as StringValue, + { type: "string", value: "binary_operator_combination" }, { type: "string", value: operatorTranslator((node as ExprNS.Binary).operator.type), - } as StringValue, + }, transform((node as ExprNS.Binary).left, declaredNames), transform((node as ExprNS.Binary).right, declaredNames), ]); case "BoolOp": return vector_to_linked_list([ - { type: "string", value: "logical_composition" } as StringValue, + { type: "string", value: "logical_composition" }, { type: "string", value: operatorTranslator((node as ExprNS.BoolOp).operator.type), - } as StringValue, + }, transform((node as ExprNS.BoolOp).left, declaredNames), transform((node as ExprNS.BoolOp).right, declaredNames), ]); case "Compare": return vector_to_linked_list([ - { type: "string", value: "comparison" } as StringValue, + { type: "string", value: "comparison" }, { type: "string", value: operatorTranslator((node as ExprNS.Compare).operator.type), - } as StringValue, + }, transform((node as ExprNS.Compare).left, declaredNames), transform((node as ExprNS.Compare).right, declaredNames), ]); case "Unary": return vector_to_linked_list([ - { type: "string", value: "unary_operator_combination" } as StringValue, + { type: "string", value: "unary_operator_combination" }, { type: "string", value: operatorTranslator((node as ExprNS.Unary).operator.type), - } as StringValue, + }, transform((node as ExprNS.Unary).right, declaredNames), ]); case "Assign": { @@ -171,45 +166,45 @@ function transform( declaredNames.add(assign.target.name.lexeme); } return vector_to_linked_list([ - { type: "string", value: isDecl ? "declaration" : "assignment" } as StringValue, + { type: "string", value: isDecl ? "declaration" : "assignment" }, transform(assign.target, declaredNames), transform(assign.value, declaredNames), ]); } case "AnnAssign": return vector_to_linked_list([ - { type: "string", value: "annotated_assignment" } as StringValue, + { type: "string", value: "annotated_assignment" }, transform((node as StmtNS.AnnAssign).target, declaredNames), transform((node as StmtNS.AnnAssign).ann, declaredNames), transform((node as StmtNS.AnnAssign).value, declaredNames), ]); case "If": return vector_to_linked_list([ - { type: "string", value: "conditional_statement" } as StringValue, + { type: "string", value: "conditional_statement" }, transform((node as StmtNS.If).condition, declaredNames), makeSequenceIfNeeded((node as StmtNS.If).body, declaredNames), makeSequenceIfNeeded((node as StmtNS.If).elseBlock, declaredNames), ]); case "Ternary": return vector_to_linked_list([ - { type: "string", value: "conditional_expression" } as StringValue, + { type: "string", value: "conditional_expression" }, transform((node as ExprNS.Ternary).predicate, declaredNames), transform((node as ExprNS.Ternary).consequent, declaredNames), transform((node as ExprNS.Ternary).alternative, declaredNames), ]); case "While": return vector_to_linked_list([ - { type: "string", value: "while_loop" } as StringValue, + { type: "string", value: "while_loop" }, transform((node as StmtNS.While).condition, declaredNames), makeSequenceIfNeeded((node as StmtNS.While).body, declaredNames), ]); case "For": { const forNode = node as StmtNS.For; return vector_to_linked_list([ - { type: "string", value: "for_loop" } as StringValue, + { type: "string", value: "for_loop" }, vector_to_linked_list([ - { type: "string", value: "name" } as StringValue, - { type: "string", value: forNode.target.lexeme } as StringValue, + { type: "string", value: "name" }, + { type: "string", value: forNode.target.lexeme }, ]), transform(forNode.iter, declaredNames), makeSequenceIfNeeded(forNode.body, declaredNames), @@ -218,10 +213,10 @@ function transform( case "FunctionDef": { const fn = node as StmtNS.FunctionDef; return vector_to_linked_list([ - { type: "string", value: "function_definition" } as StringValue, + { type: "string", value: "function_definition" }, vector_to_linked_list([ - { type: "string", value: "name" } as StringValue, - { type: "string", value: fn.name.lexeme } as StringValue, + { type: "string", value: "name" }, + { type: "string", value: fn.name.lexeme }, ]), vector_to_linked_list(fn.parameters.map(transformParam)), makeBlockIfNeeded(fn.body, declaredNames), @@ -230,10 +225,10 @@ function transform( case "Lambda": { const lam = node as ExprNS.Lambda; return vector_to_linked_list([ - { type: "string", value: "lambda_expression" } as StringValue, + { type: "string", value: "lambda_expression" }, vector_to_linked_list(lam.parameters.map(transformParam)), vector_to_linked_list([ - { type: "string", value: "return_statement" } as StringValue, + { type: "string", value: "return_statement" }, transform(lam.body, declaredNames), ]), ]); @@ -241,72 +236,72 @@ function transform( case "MultiLambda": { const lam = node as ExprNS.MultiLambda; return vector_to_linked_list([ - { type: "string", value: "lambda_expression" } as StringValue, + { type: "string", value: "lambda_expression" }, vector_to_linked_list(lam.parameters.map(transformParam)), makeBlockIfNeeded(lam.body, declaredNames), ]); } case "Return": return vector_to_linked_list([ - { type: "string", value: "return_statement" } as StringValue, + { type: "string", value: "return_statement" }, transform((node as StmtNS.Return).value, declaredNames), ]); case "Call": return vector_to_linked_list([ - { type: "string", value: "application" } as StringValue, + { type: "string", value: "application" }, transform((node as ExprNS.Call).callee, declaredNames), vector_to_linked_list((node as ExprNS.Call).args.map(arg => transform(arg, declaredNames))), ]); case "List": return vector_to_linked_list([ - { type: "string", value: "array_expression" } as StringValue, + { type: "string", value: "array_expression" }, vector_to_linked_list( (node as ExprNS.List).elements.map(el => transform(el, declaredNames)), ), ]); case "Subscript": return vector_to_linked_list([ - { type: "string", value: "object_access" } as StringValue, + { type: "string", value: "object_access" }, transform((node as ExprNS.Subscript).value, declaredNames), transform((node as ExprNS.Subscript).index, declaredNames), ]); case "Starred": return vector_to_linked_list([ - { type: "string", value: "starred_expression" } as StringValue, + { type: "string", value: "starred_expression" }, transform((node as ExprNS.Starred).value, declaredNames), ]); case "FromImport": return vector_to_linked_list([ - { type: "string", value: "import_from" } as StringValue, - { type: "string", value: (node as StmtNS.FromImport).module.lexeme } as StringValue, + { type: "string", value: "import_from" }, + { type: "string", value: (node as StmtNS.FromImport).module.lexeme }, vector_to_linked_list( (node as StmtNS.FromImport).names.map( - n => ({ type: "string", value: n.name.lexeme }) as StringValue, + n => ({ type: "string", value: n.name.lexeme }), ), ), ]); case "Global": return vector_to_linked_list([ - { type: "string", value: "global_statement" } as StringValue, - { type: "string", value: (node as StmtNS.Global).name.lexeme } as StringValue, + { type: "string", value: "global_statement" }, + { type: "string", value: (node as StmtNS.Global).name.lexeme }, ]); case "NonLocal": return vector_to_linked_list([ - { type: "string", value: "nonlocal_statement" } as StringValue, - { type: "string", value: (node as StmtNS.NonLocal).name.lexeme } as StringValue, + { type: "string", value: "nonlocal_statement" }, + { type: "string", value: (node as StmtNS.NonLocal).name.lexeme }, ]); case "Assert": return vector_to_linked_list([ - { type: "string", value: "assert_statement" } as StringValue, + { type: "string", value: "assert_statement" }, transform((node as StmtNS.Assert).value, declaredNames), ]); case "Pass": - return vector_to_linked_list([{ type: "string", value: "pass_statement" } as StringValue]); + return vector_to_linked_list([{ type: "string", value: "pass_statement" }]); case "Break": - return vector_to_linked_list([{ type: "string", value: "break_statement" } as StringValue]); + return vector_to_linked_list([{ type: "string", value: "break_statement" }]); case "Continue": return vector_to_linked_list([ - { type: "string", value: "continue_statement" } as StringValue, + { type: "string", value: "continue_statement" }, ]); default: throw new Error("Cannot transform unknown type: " + type); @@ -315,11 +310,11 @@ function transform( class ParserBuiltins { @Validate(1, 1, "parse", false) - static parse(args: Value[], source: string, command: ControlItem, context: Context): Value { + static parse(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { if (args[0].type !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "string"), + new TypeError(source, command, context, args[0].type, "string"), ); } const x = args[0].value; @@ -329,12 +324,12 @@ class ParserBuiltins { } @Validate(2, 2, "apply_in_underlying_python", false) - static apply_in_underlying_python( + static async apply_in_underlying_python( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Expr, context: Context, - ): Value { + ): Promise { const func = args[0]; const argList = args[1]; const argArray: Value[] = []; @@ -345,20 +340,20 @@ class ParserBuiltins { } if (func.type === "builtin") { - return func.func(argArray, source, command, context) as Value; + return func.func(argArray, source, command, context); } return handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, func.type, "primitive function"), + new TypeError(source, command, context, func.type, "primitive function"), ); } @Validate(1, 1, "tokenize", false) - static tokenize(args: Value[], source: string, command: ControlItem, context: Context): Value { + static tokenize(args: Value[], source: string, command: ExprNS.Call, context: Context): Value { if (args[0].type !== "string") { handleRuntimeError( context, - new TypeError(source, command as ExprNS.Expr, context, args[0].type, "string"), + new TypeError(source, command, context, args[0].type, "string"), ); } const x = args[0].value; @@ -366,7 +361,7 @@ class ParserBuiltins { const tokens: StringValue[] = []; let tok; while ((tok = pythonLexer.next())) { - tokens.push({ type: "string", value: tok.value } as StringValue); + tokens.push({ type: "string", value: tok.value }); } return vector_to_linked_list(tokens); } @@ -390,4 +385,4 @@ export default { name: GroupName.MCE, prelude: "", builtins: parserBuiltins, -} as Group; +}; diff --git a/src/stdlib/stream.ts b/src/stdlib/stream.ts index bf02e045..92f21f2d 100644 --- a/src/stdlib/stream.ts +++ b/src/stdlib/stream.ts @@ -1,9 +1,9 @@ +import { ExprNS } from "../ast-types"; import { Context } from "../engines/cse/context"; -import { ControlItem } from "../engines/cse/control"; import { BuiltinValue, ListValue, NoneValue, Value } from "../engines/cse/stash"; import { minArgMap, Validate } from "../stdlib"; import streamPrelude from "./stream.prelude"; -import { Group, GroupName } from "./utils"; +import { GroupName } from "./utils"; const streamBuiltins = new Map(); @@ -12,7 +12,7 @@ class StreamBuiltins { static stream( args: Value[], source: string, - command: ControlItem, + command: ExprNS.Call, context: Context, ): ListValue | NoneValue { if (args.length === 0) { @@ -50,4 +50,4 @@ export default { name: GroupName.STREAMS, prelude: streamPrelude, builtins: streamBuiltins, -} as Group; +}; From 60709d07228feebb62f658b31ccf176f044f2ddb Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Mon, 13 Apr 2026 21:15:26 +0800 Subject: [PATCH 11/19] Remove unused code --- package.json | 1 - src/constants.ts | 28 ------ src/engines/cse/closure.ts | 9 +- src/engines/cse/context.ts | 79 +-------------- src/engines/cse/dict.ts | 112 ---------------------- src/engines/cse/environment.ts | 27 +----- src/engines/cse/error.ts | 30 +----- src/engines/cse/heap.ts | 55 ----------- src/engines/cse/instrCreator.ts | 2 +- src/engines/cse/interpreter.ts | 17 ++-- src/engines/cse/operators.ts | 2 +- src/engines/cse/stash.ts | 4 +- src/engines/cse/types.ts | 2 +- src/engines/cse/utils.ts | 11 +-- src/engines/svml/svml-compiler.ts | 11 +-- src/engines/wasm/builderGenerator.ts | 94 +++++++++--------- src/engines/wasm/metacircularGenerator.ts | 2 +- src/errors/errors.ts | 21 ++-- src/parser/token-bridge.ts | 3 +- src/resolver/resolver.ts | 3 +- src/stdlib/parser.ts | 2 +- src/tokenizer/index.ts | 2 +- src/tokenizer/tokenizer.ts | 99 ++++++++++++++++++- src/tokens.ts | 98 ------------------- src/types/value-types.ts | 32 ------- src/validator/features/no-is-operator.ts | 2 +- 26 files changed, 186 insertions(+), 562 deletions(-) delete mode 100644 src/constants.ts delete mode 100644 src/engines/cse/dict.ts delete mode 100644 src/engines/cse/heap.ts delete mode 100644 src/tokens.ts diff --git a/package.json b/package.json index 41f11a0f..e6f7956e 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,6 @@ "compile-grammar": "nearleyc src/parser/python.ne | sed -f src/parser/python-grammar.ts.sed | prettier --stdin-filepath src/parser/python-grammar.ts > src/parser/python-grammar.ts && eslint --fix src/parser/python-grammar.ts", "build": "tsx scripts/build.ts", "dev": "tsx scripts/build.ts --watch", - "start": "yarn run build && node dist/index.js", "jsdoc": "./scripts/jsdoc.sh prepare", "jsdoc:run": "./scripts/jsdoc.sh run", "jsdoc:clean": "./scripts/jsdoc.sh clean", diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index 1150d53e..00000000 --- a/src/constants.ts +++ /dev/null @@ -1,28 +0,0 @@ -// IGNORE, not in use -import type { SourceLocation } from "./errors"; - -//import type { AcornOptions } from './parser/types' - -export const DEFAULT_ECMA_VERSION = 6; -//export const ACORN_PARSE_OPTIONS: AcornOptions = { ecmaVersion: DEFAULT_ECMA_VERSION } - -export const REQUIRE_PROVIDER_ID = "requireProvider"; -export const CUT = "cut"; // cut operator for Source 4.3 -export const TRY_AGAIN = "retry"; // command for Source 4.3 -export const GLOBAL = typeof window === "undefined" ? global : window; -export const NATIVE_STORAGE_ID = "nativeStorage"; -export const MAX_LIST_DISPLAY_LENGTH = 100; -export const UNKNOWN_LOCATION: SourceLocation = { - start: { - line: -1, - column: -1, - }, - end: { - line: -1, - column: -1, - }, -}; -export const JSSLANG_PROPERTIES = { - maxExecTime: 1000, - factorToIncreaseBy: 10, -}; diff --git a/src/engines/cse/closure.ts b/src/engines/cse/closure.ts index 70539d61..6843aeb7 100644 --- a/src/engines/cse/closure.ts +++ b/src/engines/cse/closure.ts @@ -6,11 +6,12 @@ import { StatementSequence } from "./types"; import { isNode } from "./utils"; /** - * Represents a python closure, the class is a runtime representation of a function. + * Represents a Python closure, the class is a runtime representation of a function. * Bundles the function's code (AST node) with environment in which its defined. * When Closure is called, a new environment will be created whose parent is the 'Environment' captured */ export class Closure { + /** Unique ID defined for closure */ public readonly id: string; /** AST node for function, either a 'def' or 'lambda' */ public node: StmtNS.FunctionDef | ExprNS.Lambda; @@ -22,12 +23,6 @@ export class Closure { /** Stores local variables for scope check */ public localVariables: Set; - /** Unique ID defined for closure */ - //public readonly id: string - - /** Name of the constant declaration that the closure is assigned to */ - public declaredName?: string; - constructor( node: StmtNS.FunctionDef | ExprNS.Lambda, environment: Environment, diff --git a/src/engines/cse/context.ts b/src/engines/cse/context.ts index 143fd0fe..e87b2af6 100644 --- a/src/engines/cse/context.ts +++ b/src/engines/cse/context.ts @@ -1,11 +1,10 @@ import { ConductorError } from "@sourceacademy/conductor/common"; import { StmtNS } from "../../ast-types"; +import { RuntimeSourceError } from "../../errors"; import { ModuleContext, NativeStorage } from "../../types"; import { Control } from "./control"; import { Environment } from "./environment"; -import { CseError } from "./error"; -import { Heap } from "./heap"; -import { BuiltinValue, Stash, Value } from "./stash"; +import { BuiltinValue, Stash } from "./stash"; import { ReadableContext, WritableContext } from "./streams"; import { Node } from "./types"; @@ -28,7 +27,7 @@ export class Context { stderr: WritableContext; stdin: ReadableContext; }; - public errors: CseError[] = []; + public errors: RuntimeSourceError[] = []; public moduleContexts: { [name: string]: ModuleContext }; public prelude: string | null = null; @@ -36,7 +35,6 @@ export class Context { break: boolean; debuggerOn: boolean; isRunning: boolean; - environmentTree: EnvTree; environments: Environment[]; nodes: Node[]; control: Control | null; @@ -61,15 +59,11 @@ export class Context { if (this.runtime.environments.length === 0) { const globalEnvironment = this.createGlobalEnvironment(); this.runtime.environments.push(globalEnvironment); - this.runtime.environmentTree.insert(globalEnvironment); } this.streams = this.createEmptyStreams(); this.nativeStorage = { builtins: new Map(), - previousProgramsIdentifiers: new Set(), - operators: new Map Value>(), maxExecTime: 1000, - //evaller: null, loadedModules: {}, loadedModuleTypes: {}, }; @@ -79,7 +73,6 @@ export class Context { tail: null, name: "global", head: {}, - heap: new Heap(), id: "-1", }); @@ -87,7 +80,6 @@ export class Context { break: false, debuggerOn: true, isRunning: false, - environmentTree: new EnvTree(), environments: [], value: undefined, nodes: [], @@ -126,72 +118,9 @@ export class Context { name: env.name, tail: newTail, head: { ...env.head }, - heap: new Heap(), callExpression: env.callExpression, - thisContext: env.thisContext, + closure: env.closure }; return newEnv; } } - -export class EnvTree { - private _root: EnvTreeNode | null = null; - private map = new Map(); - - get root(): EnvTreeNode | null { - return this._root; - } - - public insert(environment: Environment): void { - const tailEnvironment = environment.tail; - if (tailEnvironment === null) { - if (this._root === null) { - this._root = new EnvTreeNode(environment, null); - this.map.set(environment, this._root); - } - } else { - const parentNode = this.map.get(tailEnvironment); - if (parentNode) { - const childNode = new EnvTreeNode(environment, parentNode); - parentNode.addChild(childNode); - this.map.set(environment, childNode); - } - } - } - - public getTreeNode(environment: Environment): EnvTreeNode | undefined { - return this.map.get(environment); - } -} - -export class EnvTreeNode { - private _children: EnvTreeNode[] = []; - - constructor( - readonly environment: Environment, - public parent: EnvTreeNode | null, - ) {} - - get children(): EnvTreeNode[] { - return this._children; - } - - public resetChildren(newChildren: EnvTreeNode[]): void { - this.clearChildren(); - this.addChildren(newChildren); - newChildren.forEach(c => (c.parent = this)); - } - - private clearChildren(): void { - this._children = []; - } - - private addChildren(newChildren: EnvTreeNode[]): void { - this._children.push(...newChildren); - } - - public addChild(newChild: EnvTreeNode): EnvTreeNode { - this._children.push(newChild); - return newChild; - } -} diff --git a/src/engines/cse/dict.ts b/src/engines/cse/dict.ts deleted file mode 100644 index e3f4d19f..00000000 --- a/src/engines/cse/dict.ts +++ /dev/null @@ -1,112 +0,0 @@ -// import * as es from 'estree' -// import { isImportDeclaration, getModuleDeclarationSource } from './utils'; - -/** - * Python style dictionary - */ -export default class Dict { - constructor(private readonly internalMap = new Map()) {} - - public get size() { - return this.internalMap.size; - } - - public [Symbol.iterator]() { - return this.internalMap[Symbol.iterator](); - } - - public get(key: K) { - return this.internalMap.get(key); - } - - public set(key: K, value: V) { - return this.internalMap.set(key, value); - } - - public has(key: K) { - return this.internalMap.has(key); - } - - /** - * Similar to how the python dictionary's setdefault function works: - * If the key is not present, it is set to the given value, then that value is returned - * Otherwise, `setdefault` returns the value stored in the dictionary without - * modifying it - */ - public setdefault(key: K, value: V) { - if (!this.has(key)) { - this.set(key, value); - } - - return this.get(key)!; - } - - public update(key: K, defaultVal: V, updater: (oldV: V) => V) { - const value = this.setdefault(key, defaultVal); - const newValue = updater(value); - this.set(key, newValue); - return newValue; - } - - public entries() { - return [...this.internalMap.entries()]; - } - - public forEach(func: (key: K, value: V) => void) { - this.internalMap.forEach((v, k) => func(k, v)); - } - - /** - * Similar to `mapAsync`, but for an async mapping function that does not return any value - */ - public async forEachAsync(func: (k: K, v: V, index: number) => Promise): Promise { - await Promise.all(this.map((key, value, i) => func(key, value, i))); - } - - public map(func: (key: K, value: V, index: number) => T) { - return this.entries().map(([k, v], i) => func(k, v, i)); - } - - /** - * Using a mapping function that returns a promise, transform a map - * to another map with different keys and values. All calls to the mapping function - * execute asynchronously - */ - public mapAsync(func: (key: K, value: V, index: number) => Promise) { - return Promise.all(this.map((key, value, i) => func(key, value, i))); - } - - public flatMap(func: (key: K, value: V, index: number) => U[]) { - return this.entries().flatMap(([k, v], i) => func(k, v, i)); - } -} - -/** - * Convenience class for maps that store an array of values - */ -export class ArrayMap extends Dict { - public add(key: K, item: V) { - this.setdefault(key, []).push(item); - } -} - -// export function filterImportDeclarations({ -// body -// }: es.Program): [ -// ArrayMap, -// Exclude[] -// ] { -// return body.reduce( -// ([importNodes, otherNodes], node) => { -// if (!isImportDeclaration(node)) return [importNodes, [...otherNodes, node]] - -// const moduleName = getModuleDeclarationSource(node) -// importNodes.add(moduleName, node) -// return [importNodes, otherNodes] -// }, -// [new ArrayMap(), []] as [ -// ArrayMap, -// Exclude[] -// ] -// ) -// } diff --git a/src/engines/cse/environment.ts b/src/engines/cse/environment.ts index 47665804..ae917cf7 100644 --- a/src/engines/cse/environment.ts +++ b/src/engines/cse/environment.ts @@ -3,7 +3,6 @@ import { MissingRequiredPositionalError, TooManyPositionalArgumentsError } from import { Closure } from "./closure"; import { Context } from "./context"; import { handleRuntimeError } from "./error"; -import { Heap } from "./heap"; import { Value } from "./stash"; export interface Frame { @@ -38,9 +37,6 @@ export interface Environment { */ head: Frame; - heap: Heap; - thisContext?: Value; - /** * The closure associated with this environment, if this environment was created as part of a function call. */ @@ -65,14 +61,11 @@ export const createEnvironment = ( : "lambda", tail: closure.environment, head: {}, - heap: new Heap(), id: uniqueId(context), callExpression: callExpression, closure: closure, }; - - // console.info('closure.node.params:', closure.node.params); - // console.info('Number of params:', closure.node.params.length); + const isVariadic = closure.node.parameters.some(param => param.isStarred); let consumed = false; closure.node.parameters.forEach((paramToken, index) => { @@ -133,8 +126,7 @@ export const createSimpleEnvironment = ( name, tail, head: {}, - heap: new Heap(), - // TODO: callExpression and thisContext are optional and can be provided as needed. + // TODO: callExpression is optional and can be provided as needed. }; }; @@ -154,24 +146,10 @@ export const createBlockEnvironment = ( name, tail: currentEnvironment(context), head: {}, - heap: new Heap(), id: uniqueId(context), }; }; -// export const handleArrayCreation = ( -// context: Context, -// array: Value[], -// envOverride?: Environment -// ): void => { -// const environment = envOverride ?? currentEnvironment(context) -// Object.defineProperties(array, { -// id: { value: uniqueId(context) }, -// environment: { value: environment, writable: true } -// }) -// environment.heap.add(array) -// } - export const currentEnvironment = (context: Context): Environment => { return context.runtime.environments[0]; }; @@ -190,5 +168,4 @@ export const popEnvironment = (context: Context) => context.runtime.environments export const pushEnvironment = (context: Context, environment: Environment) => { context.runtime.environments.unshift(environment); - context.runtime.environmentTree.insert(environment); }; diff --git a/src/engines/cse/error.ts b/src/engines/cse/error.ts index 95d97b37..519d556b 100644 --- a/src/engines/cse/error.ts +++ b/src/engines/cse/error.ts @@ -1,33 +1,7 @@ import { - ErrorSeverity, - ErrorType, - RuntimeSourceError, - SourceError, - SourceLocation, - UNKNOWN_LOCATION, + RuntimeSourceError } from "../../errors"; -import { Context } from "./context"; - -export class CseError implements SourceError { - public type = ErrorType.RUNTIME; - public severity = ErrorSeverity.ERROR; - public location: SourceLocation; - - constructor( - public message: string, - location?: SourceLocation, - ) { - this.location = location ?? UNKNOWN_LOCATION; - } - - public explain() { - return this.message; - } - - public elaborate() { - return "There is an error in the CSE machine."; - } -} +import type { Context } from "./context"; export function handleRuntimeError(context: Context, error: RuntimeSourceError): never { context.errors.push(error); diff --git a/src/engines/cse/heap.ts b/src/engines/cse/heap.ts deleted file mode 100644 index 4c3d0068..00000000 --- a/src/engines/cse/heap.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Closure } from "./closure"; -import { Environment } from "./environment"; -import { Value } from "./stash"; - -// Every array also has the properties `id` and `environment` for use in the frontend CSE Machine -export type EnvArray = (Value & { - readonly id: string; - environment: Environment; -})[]; - -// Objects in the heap can only store arrays or closures -export type HeapObject = EnvArray | Closure; - -/** - * The heap stores all objects in each environment. - */ -export class Heap { - private storage: Set | null = null; - - public constructor() {} - - add(...items: T[]): void { - this.storage ??= new Set(); - for (const item of items) { - this.storage.add(item); - } - } - - /** Checks the existence of `item` in the heap. */ - contains(item: T): boolean { - return this.storage?.has(item) ?? false; - } - - /** Gets the number of items in the heap. */ - size(): number { - return this.storage?.size ?? 0; - } - - /** - * Removes `item` from current heap and adds it to `otherHeap`. - * If the current heap does not contain `item`, nothing happens. - * @returns whether the item transfer is successful - */ - move(item: T, otherHeap: Heap): boolean { - if (!this.contains(item)) return false; - this.storage!.delete(item); - otherHeap.add(item); - return true; - } - - /** Returns a copy of the heap's contents. */ - getHeap(): Set { - return new Set(this.storage); - } -} diff --git a/src/engines/cse/instrCreator.ts b/src/engines/cse/instrCreator.ts index 36377683..a888f8e6 100644 --- a/src/engines/cse/instrCreator.ts +++ b/src/engines/cse/instrCreator.ts @@ -1,5 +1,5 @@ import { ExprNS } from "../../ast-types"; -import { TokenType } from "../../tokens"; +import { TokenType } from "../../tokenizer"; import { Environment } from "./environment"; import { AppInstr, diff --git a/src/engines/cse/interpreter.ts b/src/engines/cse/interpreter.ts index 7be8df98..b37c5ddc 100644 --- a/src/engines/cse/interpreter.ts +++ b/src/engines/cse/interpreter.ts @@ -10,10 +10,10 @@ import { ErrorType } from "@sourceacademy/conductor/common"; import { ExprNS, StmtNS } from "../../ast-types"; import * as error from "../../errors/errors"; import { BuiltinReassignmentError, UnsupportedOperandTypeError } from "../../errors/errors"; -import { builtIns, toPythonString } from "../../stdlib"; +import { builtIns } from "../../stdlib"; import { Group } from "../../stdlib/utils"; -import { TokenType } from "../../tokens"; -import { CSEBreak, RecursivePartial, Representation, Result } from "../../types"; +import { TokenType } from "../../tokenizer"; +import { CSEBreak, RecursivePartial, Result } from "../../types"; import { Closure, isStatementSequence } from "./closure"; import { Context } from "./context"; import { Control, ControlItem } from "./control"; @@ -83,12 +83,9 @@ export function CSEResultPromise(context: Context, value: Value): Promise Value) - | ((args: Value[], code: string, command: ExprNS.Expr, context: Context) => Promise); + | ((args: Value[], code: string, command: ExprNS.Call, context: Context) => Value) + | ((args: Value[], code: string, command: ExprNS.Call, context: Context) => Promise); minArgs: number; } diff --git a/src/engines/cse/types.ts b/src/engines/cse/types.ts index 9e96159d..0bb612be 100644 --- a/src/engines/cse/types.ts +++ b/src/engines/cse/types.ts @@ -1,5 +1,5 @@ import { ExprNS, StmtNS } from "../../ast-types"; -import { TokenType } from "../../tokens"; +import { TokenType } from "../../tokenizer"; import { Environment } from "./environment"; import { Value } from "./stash"; diff --git a/src/engines/cse/utils.ts b/src/engines/cse/utils.ts index 73a08752..00da7962 100644 --- a/src/engines/cse/utils.ts +++ b/src/engines/cse/utils.ts @@ -8,8 +8,7 @@ import { UnboundLocalError, } from "../../errors/errors"; import { builtInConstants, builtIns } from "../../stdlib"; -import { Token } from "../../tokenizer"; -import { TokenType } from "../../tokens"; +import { Token, TokenType } from "../../tokenizer"; import { Context } from "./context"; import { Control, ControlItem } from "./control"; import { currentEnvironment, Environment } from "./environment"; @@ -302,14 +301,6 @@ export const checkStackOverFlow = (_context: Context, _control: Control) => { // TODO }; -// export const isSimpleFunction = (node: ) => { -// if (node.body.type !== 'BlockStatement' && node.body.type !== 'StatementSequence') { -// return true -// } else { -// const block = node.body -// return block.body.length === 1 && block.body[0].type === 'ReturnStatement' -// } -// } export function pythonMod(a: bigint, b: bigint): bigint; export function pythonMod(a: number, b: number): number; export function pythonMod(a: number | bigint, b: number | bigint): number | bigint { diff --git a/src/engines/svml/svml-compiler.ts b/src/engines/svml/svml-compiler.ts index e038617f..bf32b409 100644 --- a/src/engines/svml/svml-compiler.ts +++ b/src/engines/svml/svml-compiler.ts @@ -1,11 +1,10 @@ -import { StmtNS, ExprNS } from "../../ast-types"; -import { Token } from "../../tokenizer"; -import { TokenType } from "../../tokens"; -import { PRIMITIVE_FUNCTIONS } from "./builtins"; -import { SVMLProgram } from "./types"; +import { ExprNS, StmtNS } from "../../ast-types"; +import { Environment, FunctionEnvironments, Resolver } from "../../resolver"; +import { Token, TokenType } from "../../tokenizer"; import { SVMLIRBuilder } from "./SVMLIRBuilder"; +import { PRIMITIVE_FUNCTIONS } from "./builtins"; import OpCodes from "./opcodes"; -import { FunctionEnvironments, Environment, Resolver } from "../../resolver"; +import { SVMLProgram } from "./types"; /** Signed 32-bit integer bounds used to decide LGCI vs LGCF64 encoding. */ const I32_MIN = -2_147_483_648; diff --git a/src/engines/wasm/builderGenerator.ts b/src/engines/wasm/builderGenerator.ts index 4103c68e..e154472f 100644 --- a/src/engines/wasm/builderGenerator.ts +++ b/src/engines/wasm/builderGenerator.ts @@ -1,55 +1,55 @@ import { - f64, - global, - i32, - i64, - local, - mut, - wasm, - WasmCall, - WasmData, - WasmExport, - type WasmInstruction, - type WasmNumeric, - type WasmRaw, + f64, + global, + i32, + i64, + local, + mut, + wasm, + WasmCall, + WasmData, + WasmExport, + type WasmInstruction, + type WasmNumeric, + type WasmRaw, } from "@sourceacademy/wasm-util"; import { WasmExports } from "."; import { ExprNS, StmtNS } from "../../ast-types"; -import { TokenType } from "../../tokens"; +import { TokenType } from "../../tokenizer"; import { - ALLOC_ENV_FX, - APPLY_FX_NAME, - applyFuncFactory, - ARITHMETIC_OP_FX, - ARITHMETIC_OP_TAG, - BOOL_NOT_FX, - BOOLISE_FX, - COMPARISON_OP_FX, - COMPARISON_OP_TAG, - CURR_ENV, - ENV_HEAD_SIZE, - GET_LEX_ADDR_FX, - GET_LIST_ELEMENT_FX, - HEAP_PTR, - importedLogs, - LOG_FX, - MAKE_BOOL_FX, - MAKE_CLOSURE_FX, - MAKE_COMPLEX_FX, - MAKE_FLOAT_FX, - MAKE_INT_FX, - MAKE_LIST_FX, - MAKE_NONE_FX, - MAKE_PAIR_FX, - MAKE_STRING_FX, - MALLOC_FX, - nativeFunctions, - NEG_FX, - PRE_APPLY_FX, - RETURN_ENV_NAME, - SET_CONTIGUOUS_BLOCK_FX, - SET_LEX_ADDR_FX, - SET_LIST_ELEMENT_FX, + ALLOC_ENV_FX, + APPLY_FX_NAME, + applyFuncFactory, + ARITHMETIC_OP_FX, + ARITHMETIC_OP_TAG, + BOOL_NOT_FX, + BOOLISE_FX, + COMPARISON_OP_FX, + COMPARISON_OP_TAG, + CURR_ENV, + ENV_HEAD_SIZE, + GET_LEX_ADDR_FX, + GET_LIST_ELEMENT_FX, + HEAP_PTR, + importedLogs, + LOG_FX, + MAKE_BOOL_FX, + MAKE_CLOSURE_FX, + MAKE_COMPLEX_FX, + MAKE_FLOAT_FX, + MAKE_INT_FX, + MAKE_LIST_FX, + MAKE_NONE_FX, + MAKE_PAIR_FX, + MAKE_STRING_FX, + MALLOC_FX, + nativeFunctions, + NEG_FX, + PRE_APPLY_FX, + RETURN_ENV_NAME, + SET_CONTIGUOUS_BLOCK_FX, + SET_LEX_ADDR_FX, + SET_LIST_ELEMENT_FX, } from "./constants"; import { LibFuncType } from "./library"; diff --git a/src/engines/wasm/metacircularGenerator.ts b/src/engines/wasm/metacircularGenerator.ts index 9a4a99e3..ded03e4f 100644 --- a/src/engines/wasm/metacircularGenerator.ts +++ b/src/engines/wasm/metacircularGenerator.ts @@ -1,6 +1,6 @@ import { PARSE_TREE_STRINGS, WasmExports } from "."; import { ExprNS, StmtNS } from "../../ast-types"; -import { TokenType } from "../../tokens"; +import { TokenType } from "../../tokenizer"; interface BuilderVisitor extends StmtNS.Visitor, ExprNS.Visitor { visit(stmt: StmtNS.Stmt): S; diff --git a/src/errors/errors.ts b/src/errors/errors.ts index 72e010cd..04e2d7f7 100644 --- a/src/errors/errors.ts +++ b/src/errors/errors.ts @@ -1,12 +1,8 @@ import { ExprNS, StmtNS } from "../ast-types"; -import { isStatementSequence } from "../engines/cse/closure"; import { Context } from "../engines/cse/context"; -import { ControlItem } from "../engines/cse/control"; import { operatorTranslator } from "../engines/cse/types"; -import { isNode } from "../engines/cse/utils"; import { Token } from "../tokenizer"; -import { TokenType } from "../tokens"; - +import { TokenType } from "../tokenizer/tokenizer"; export enum ErrorType { IMPORT = "Import", RUNTIME = "Runtime", @@ -380,21 +376,16 @@ export class ZeroDivisionError extends RuntimeSourceError { } export class StepLimitExceededError extends RuntimeSourceError { - constructor(source: string, node: ControlItem) { - const srcNode = isNode(node) ? node : node.srcNode; - const locatable = isStatementSequence(srcNode) - ? srcNode.body[0] - : srcNode; - - super(locatable); + constructor(source: string, node: ExprNS.Expr | StmtNS.Stmt) { + super(node); this.type = ErrorType.RUNTIME; - const index = locatable.startToken.indexInSource; + const index = node.startToken.indexInSource; const { lineIndex, fullLine } = getFullLine(source, index); const errorPos = - "operator" in locatable && locatable.operator instanceof Token - ? locatable.operator.indexInSource - locatable.startToken.indexInSource + "operator" in node && node.operator instanceof Token + ? node.operator.indexInSource - node.startToken.indexInSource : 0; const indicator = createErrorIndicator(fullLine, errorPos); // no target symbol diff --git a/src/parser/token-bridge.ts b/src/parser/token-bridge.ts index 4cdd8c70..c418621d 100644 --- a/src/parser/token-bridge.ts +++ b/src/parser/token-bridge.ts @@ -1,6 +1,5 @@ import { FunctionParam } from "../ast-types"; -import { Token } from "../tokenizer/tokenizer"; -import { TokenType } from "../tokens"; +import { Token, TokenType } from "../tokenizer/tokenizer"; const MOO_TO_TOKEN_TYPE: Record = { name: TokenType.NAME, diff --git a/src/resolver/resolver.ts b/src/resolver/resolver.ts index b3ff24c9..251532e6 100644 --- a/src/resolver/resolver.ts +++ b/src/resolver/resolver.ts @@ -1,8 +1,7 @@ import { ExprNS, StmtNS } from "../ast-types"; import constants from "../stdlib/py_s1_constants.json"; import { Group } from "../stdlib/utils"; -import { Token } from "../tokenizer/tokenizer"; -import { TokenType } from "../tokens"; +import { Token, TokenType } from "../tokenizer/tokenizer"; import { FeatureValidator } from "../validator/types"; import { ResolverErrors } from "./errors"; type Expr = ExprNS.Expr; diff --git a/src/stdlib/parser.ts b/src/stdlib/parser.ts index a8fde4fe..2a229833 100644 --- a/src/stdlib/parser.ts +++ b/src/stdlib/parser.ts @@ -327,7 +327,7 @@ class ParserBuiltins { static async apply_in_underlying_python( args: Value[], source: string, - command: ExprNS.Expr, + command: ExprNS.Call, context: Context, ): Promise { const func = args[0]; diff --git a/src/tokenizer/index.ts b/src/tokenizer/index.ts index 1fb6a44c..ea1516bd 100644 --- a/src/tokenizer/index.ts +++ b/src/tokenizer/index.ts @@ -1 +1 @@ -export { Token } from "./tokenizer"; +export { Token, TokenType } from "./tokenizer"; diff --git a/src/tokenizer/tokenizer.ts b/src/tokenizer/tokenizer.ts index 3231dbf5..8b1259d3 100644 --- a/src/tokenizer/tokenizer.ts +++ b/src/tokenizer/tokenizer.ts @@ -1,4 +1,101 @@ -import { TokenType } from "../tokens"; +export enum TokenType { + ENDMARKER = 0, + NAME = 1, + NUMBER = 2, + BIGINT = 3, + STRING = 4, + NEWLINE = 5, + INDENT = 6, + DEDENT = 7, + LPAR = 8, + RPAR = 9, + COLON = 10, + DOUBLECOLON = 11, + COMMA = 12, + PLUS = 13, + MINUS = 14, + BANG = 15, + STAR = 16, + SLASH = 17, + VBAR = 18, + AMPER = 19, + LESS = 20, + GREATER = 21, + EQUAL = 22, + PERCENT = 23, + DOUBLEEQUAL = 24, + NOTEQUAL = 25, + LESSEQUAL = 26, + GREATEREQUAL = 27, + DOUBLESTAR = 28, + COMPLEX = 29, + AND = 30, + OR = 31, + FOR = 32, + WHILE = 33, + NONE = 34, + TRUE = 35, + FALSE = 36, + IS = 37, + NOT = 38, + ISNOT = 39, + PASS = 40, + DEF = 41, + LAMBDA = 42, + FROM = 43, + DOUBLESLASH = 44, + BREAK = 45, + CONTINUE = 46, + RETURN = 47, + ASSERT = 48, + IMPORT = 49, + GLOBAL = 50, + NONLOCAL = 51, + IF = 52, + ELSE = 53, + ELIF = 54, + IN = 55, + NOTIN = 56, + RSQB = 57, + LSQB = 58, + ELLIPSIS = 59, + SEMI = 60, + DOT = 61, + LBRACE = 62, + RBRACE = 63, + TILDE = 64, + CIRCUMFLEX = 65, + LEFTSHIFT = 66, + RIGHTSHIFT = 67, + PLUSEQUAL = 68, + MINEQUAL = 69, + STAREQUAL = 70, + SLASHEQUAL = 71, + PERCENTEQUAL = 72, + AMPEREQUAL = 73, + VBAREQUAL = 74, + CIRCUMFLEXEQUAL = 75, + LEFTSHIFTEQUAL = 76, + RIGHTSHIFTEQUAL = 77, + DOUBLESTAREQUAL = 78, + DOUBLESLASHEQUAL = 79, + AT = 80, + ATEQUAL = 81, + RARROW = 82, + COLONEQUAL = 83, + OP = 84, + AWAIT = 85, + ASYNC = 86, + TYPE_IGNORE = 87, + TYPE_COMMENT = 88, + YIELD = 89, + WITH = 90, + DEL = 91, + TRY = 92, + EXCEPT = 93, + FINALLY = 94, + RAISE = 95, +} /** * Represents a token produced by the lexer. diff --git a/src/tokens.ts b/src/tokens.ts deleted file mode 100644 index 000dcf1d..00000000 --- a/src/tokens.ts +++ /dev/null @@ -1,98 +0,0 @@ -export enum TokenType { - ENDMARKER = 0, - NAME = 1, - NUMBER = 2, - BIGINT = 3, - STRING = 4, - NEWLINE = 5, - INDENT = 6, - DEDENT = 7, - LPAR = 8, - RPAR = 9, - COLON = 10, - DOUBLECOLON = 11, - COMMA = 12, - PLUS = 13, - MINUS = 14, - BANG = 15, - STAR = 16, - SLASH = 17, - VBAR = 18, - AMPER = 19, - LESS = 20, - GREATER = 21, - EQUAL = 22, - PERCENT = 23, - DOUBLEEQUAL = 24, - NOTEQUAL = 25, - LESSEQUAL = 26, - GREATEREQUAL = 27, - DOUBLESTAR = 28, - COMPLEX = 29, - AND = 30, - OR = 31, - FOR = 32, - WHILE = 33, - NONE = 34, - TRUE = 35, - FALSE = 36, - IS = 37, - NOT = 38, - ISNOT = 39, - PASS = 40, - DEF = 41, - LAMBDA = 42, - FROM = 43, - DOUBLESLASH = 44, - BREAK = 45, - CONTINUE = 46, - RETURN = 47, - ASSERT = 48, - IMPORT = 49, - GLOBAL = 50, - NONLOCAL = 51, - IF = 52, - ELSE = 53, - ELIF = 54, - IN = 55, - NOTIN = 56, - RSQB = 57, - LSQB = 58, - ELLIPSIS = 59, - SEMI = 60, - DOT = 61, - LBRACE = 62, - RBRACE = 63, - TILDE = 64, - CIRCUMFLEX = 65, - LEFTSHIFT = 66, - RIGHTSHIFT = 67, - PLUSEQUAL = 68, - MINEQUAL = 69, - STAREQUAL = 70, - SLASHEQUAL = 71, - PERCENTEQUAL = 72, - AMPEREQUAL = 73, - VBAREQUAL = 74, - CIRCUMFLEXEQUAL = 75, - LEFTSHIFTEQUAL = 76, - RIGHTSHIFTEQUAL = 77, - DOUBLESTAREQUAL = 78, - DOUBLESLASHEQUAL = 79, - AT = 80, - ATEQUAL = 81, - RARROW = 82, - COLONEQUAL = 83, - OP = 84, - AWAIT = 85, - ASYNC = 86, - TYPE_IGNORE = 87, - TYPE_COMMENT = 88, - YIELD = 89, - WITH = 90, - DEL = 91, - TRY = 92, - EXCEPT = 93, - FINALLY = 94, - RAISE = 95, -} diff --git a/src/types/value-types.ts b/src/types/value-types.ts index b2437506..ccf4e600 100644 --- a/src/types/value-types.ts +++ b/src/types/value-types.ts @@ -4,7 +4,6 @@ import { handleRuntimeError } from "../engines/cse/error"; import { BuiltinValue, Value } from "../engines/cse/stash"; import { ValueError, ZeroDivisionError } from "../errors"; import { ModuleFunctions } from "../modules/moduleTypes"; -import { toPythonString } from "../stdlib"; export class CSEBreak {} @@ -274,14 +273,6 @@ export type RecursivePartial = // The CSE machine either finishes evaluating (to an error or a result) or it has a suspended evaluation. export type Result = Finished | SuspendedCseEval; -// TODO: should allow debug -// export interface Suspended { -// status: 'suspended' -// it: IterableIterator -// scheduler: Scheduler -// context: Context -// } - export interface SuspendedCseEval { status: "suspended-cse-eval"; context: Context; @@ -291,34 +282,11 @@ export interface Finished { status: "finished"; context: Context; value: Value; - representation: Representation; // if the returned value needs a unique representation, - // (for example if the language used is not JS), - // the display of the result will use the representation - // field instead -} - -export class Representation { - constructor(public representation: string) {} - - toString(value: Value): string { - // call str(value) in stdlib - // TODO: mapping - const result = toPythonString(value); - return result; - } } export interface NativeStorage { builtins: Map; - previousProgramsIdentifiers: Set; - operators: Map Value>; maxExecTime: number; - //evaller: null | ((program: string) => Value) - /* - the first time evaller is used, it must be used directly like `eval(code)` to inherit - surrounding scope, so we cannot set evaller to `eval` directly. subsequent assignments to evaller will - close in the surrounding values, so no problem - */ loadedModules: Record; loadedModuleTypes: Record>; } diff --git a/src/validator/features/no-is-operator.ts b/src/validator/features/no-is-operator.ts index f31ecab9..b23eed10 100644 --- a/src/validator/features/no-is-operator.ts +++ b/src/validator/features/no-is-operator.ts @@ -1,5 +1,5 @@ import { ExprNS } from "../../ast-types"; -import { TokenType } from "../../tokens"; +import { TokenType } from "../../tokenizer"; import { ASTNode, FeatureNotSupportedError, FeatureValidator } from "../types"; export const NoIsOperatorValidator: FeatureValidator = { From 4c5a04e048bb88f80ffed07763662dcc9c5d6d6a Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Tue, 14 Apr 2026 23:13:13 +0800 Subject: [PATCH 12/19] Fix some bugs (prelude functions not working + `==` not working for expanded equality + mismatched name in Python 4 stdlib) --- src/conductor/PyCseEvaluator.ts | 5 ++--- src/engines/cse/operators.ts | 5 ++--- src/stdlib/parser.ts | 23 ++++++++++++++++++++++- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/conductor/PyCseEvaluator.ts b/src/conductor/PyCseEvaluator.ts index b0c31eb3..fbb714af 100644 --- a/src/conductor/PyCseEvaluator.ts +++ b/src/conductor/PyCseEvaluator.ts @@ -68,10 +68,9 @@ abstract class PyCseEvaluatorBase extends BasicEvaluator { }; await this.ensurePreludesLoaded(); - const script = chunk + "\n"; const ast = parse(script); - const errors = analyze(ast, script, this.variant, this.groups); + const errors = analyze(ast, script, this.variant, this.groups, this.context.runtime.environments.filter(env => env.name === "prelude").map(env => Object.keys(env.head)).flat()); if (errors.length > 0) { for (const error of errors.slice(0, -1)) { @@ -80,7 +79,7 @@ abstract class PyCseEvaluatorBase extends BasicEvaluator { throw errors[errors.length - 1]; } - await evaluate("", ast, this.context, { + await evaluate(script, ast, this.context, { variant: this.variant, groups: this.groups, }); diff --git a/src/engines/cse/operators.ts b/src/engines/cse/operators.ts index 3452066d..401f0584 100644 --- a/src/engines/cse/operators.ts +++ b/src/engines/cse/operators.ts @@ -184,9 +184,8 @@ export function handleExpandedEquality( // Some types have value-based equality (e.g. strings), while others have reference-based equality (e.g. lists). if ("value" in left && "value" in right) { - if (left.value !== right.value) { - return { type: "bool", value: operator == TokenType.NOTEQUAL }; - } + return { type: "bool", value: (left.value === right.value) !== (operator == TokenType.NOTEQUAL) }; + } return { type: "bool", value: (operator == TokenType.NOTEQUAL) !== (left == right) }; } diff --git a/src/stdlib/parser.ts b/src/stdlib/parser.ts index 2a229833..96f75a75 100644 --- a/src/stdlib/parser.ts +++ b/src/stdlib/parser.ts @@ -1,19 +1,38 @@ import { ExprNS, FunctionParam, StmtNS } from "../ast-types"; import { Context } from "../engines/cse/context"; +<<<<<<< Updated upstream import { handleRuntimeError } from "../engines/cse/error"; import { +======= +import { ControlItem } from "../engines/cse/control"; +import { handleRuntimeError } from "../engines/cse/error"; +import { + BigIntValue, + BoolValue, +>>>>>>> Stashed changes BuiltinValue, + ComplexValue, ListValue, NoneValue, +<<<<<<< Updated upstream StringValue, Value +======= + NumberValue, + StringValue, + Value, +>>>>>>> Stashed changes } from "../engines/cse/stash"; import { operatorTranslator } from "../engines/cse/types"; import { TypeError } from "../errors/errors"; import { parse } from "../parser"; import pythonLexer from "../parser/lexer"; import { minArgMap, Validate } from "../stdlib"; +<<<<<<< Updated upstream import { GroupName } from "./utils"; +======= +import { Group, GroupName } from "./utils"; +>>>>>>> Stashed changes const None: NoneValue = { type: "none" }; @@ -178,6 +197,8 @@ function transform( transform((node as StmtNS.AnnAssign).ann, declaredNames), transform((node as StmtNS.AnnAssign).value, declaredNames), ]); + case "Grouping": + return transform((node as ExprNS.Grouping).expression, declaredNames); case "If": return vector_to_linked_list([ { type: "string", value: "conditional_statement" }, @@ -213,7 +234,7 @@ function transform( case "FunctionDef": { const fn = node as StmtNS.FunctionDef; return vector_to_linked_list([ - { type: "string", value: "function_definition" }, + { type: "string", value: "function_declaration" }, vector_to_linked_list([ { type: "string", value: "name" }, { type: "string", value: fn.name.lexeme }, From 1fac0bbc095a186da50f068f8791354621cca950 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Wed, 15 Apr 2026 10:40:01 +0800 Subject: [PATCH 13/19] Add explicit types for every evaluator + fix test cases for parser --- src/conductor/PyCseEvaluator.ts | 1 + src/engines/cse/error.ts | 8 ++ src/engines/cse/instrCreator.ts | 12 +- src/engines/cse/interpreter.ts | 199 ++++++++++++++++---------------- src/engines/cse/types.ts | 32 +---- src/engines/cse/utils.ts | 1 - src/stdlib/parser.ts | 19 --- src/tests/parser-stdlib.test.ts | 26 ++--- 8 files changed, 132 insertions(+), 166 deletions(-) diff --git a/src/conductor/PyCseEvaluator.ts b/src/conductor/PyCseEvaluator.ts index fbb714af..af3716e9 100644 --- a/src/conductor/PyCseEvaluator.ts +++ b/src/conductor/PyCseEvaluator.ts @@ -55,6 +55,7 @@ abstract class PyCseEvaluatorBase extends BasicEvaluator { }); } } + console.log(this.context.runtime.environments); }); } diff --git a/src/engines/cse/error.ts b/src/engines/cse/error.ts index 519d556b..c818716f 100644 --- a/src/engines/cse/error.ts +++ b/src/engines/cse/error.ts @@ -21,3 +21,11 @@ export class AssertionError extends RuntimeSourceError { return "Please contact the administrators to let them know that this error has occurred"; } } + +export class UnknownEvaluatorError extends RuntimeSourceError { + constructor(public readonly evaluatorName: string) { + super(); + this.message = `Unknown evaluator error: ${evaluatorName}\nIf you see this error, please report it to the administrators.`; + } +} + diff --git a/src/engines/cse/instrCreator.ts b/src/engines/cse/instrCreator.ts index a888f8e6..2b345741 100644 --- a/src/engines/cse/instrCreator.ts +++ b/src/engines/cse/instrCreator.ts @@ -12,18 +12,19 @@ import { EndOfFunctionBodyInstr, EnvInstr, ForInstr, - Instr, InstrType, ListAccessInstr, ListAssmtInstr, ListInstr, Node, + PopInstr, + ResetInstr, StatementSequence, UnOpInstr, WhileInstr, } from "./types"; -export const popInstr = (srcNode: Node): Instr => ({ instrType: InstrType.POP, srcNode }); +export const popInstr = (srcNode: Node): PopInstr => ({ instrType: InstrType.POP, srcNode }); export const assmtInstr = ( symbol: string, @@ -60,11 +61,6 @@ export const envInstr = (env: Environment, srcNode: Node): EnvInstr => ({ srcNode, }); -export const markerInstr = (srcNode: Node): Instr => ({ - instrType: InstrType.MARKER, - srcNode, -}); - export const continueInstr = (srcNode: Node): ContinueInstr => ({ instrType: InstrType.CONTINUE, srcNode, @@ -76,7 +72,7 @@ export const binOpInstr = (symbol: TokenType, srcNode: Node): BinOpInstr => ({ srcNode, }); -export const resetInstr = (srcNode: Node): Instr => ({ +export const resetInstr = (srcNode: Node): ResetInstr => ({ instrType: InstrType.RESET, srcNode, }); diff --git a/src/engines/cse/interpreter.ts b/src/engines/cse/interpreter.ts index b37c5ddc..99c2aa76 100644 --- a/src/engines/cse/interpreter.ts +++ b/src/engines/cse/interpreter.ts @@ -24,7 +24,7 @@ import { popEnvironment, pushEnvironment, } from "./environment"; -import { handleRuntimeError } from "./error"; +import { handleRuntimeError, UnknownEvaluatorError } from "./error"; import * as instrCreator from "./instrCreator"; import { evaluateBinaryExpression, evaluateUnaryExpression, isFalsy } from "./operators"; import { Stash, Value } from "./stash"; @@ -35,14 +35,19 @@ import { BinOpInstr, BoolOpInstr, BranchInstr, + BreakInstr, + ContinueInstr, + EndOfFunctionBodyInstr, EnvInstr, + Instr, InstrType, ListAccessInstr, ListAssmtInstr, ListInstr, - Node, + PopInstr, + ResetInstr, UnOpInstr, - WhileInstr, + WhileInstr } from "./types"; import { envChanging, @@ -61,9 +66,9 @@ export interface IOptions { variant: number; } -type CmdEvaluator = ( +type CmdEvaluator = ( code: string, - command: ControlItem, + command: T, context: Context, control: Control, stash: Stash, @@ -287,11 +292,19 @@ export async function* generateCSEMachineStateStream( if (isNode(command)) { const node = command; - const nodeType = node.kind; context.runtime.nodes.shift(); context.runtime.nodes.unshift(command); - await cmdEvaluators[nodeType](code, command, context, control, stash, isPrelude, variant); + const nodeKind = node.kind; + if (!isDeclaredEvaluator(nodeKind)) { + handleRuntimeError( + context, + new UnknownEvaluatorError( + node.kind + ) + ); + } + await cmdEvaluators[nodeKind](code, node as never, context, control, stash, isPrelude, variant); if (context.runtime.break && context.runtime.debuggerOn) { // TODO @@ -303,10 +316,18 @@ export async function* generateCSEMachineStateStream( } } else { // Command is an instruction - const instr = command; - await cmdEvaluators[instr.instrType]( + const instrType = command.instrType; + if (!isDeclaredEvaluator(instrType)) { + handleRuntimeError( + context, + new UnknownEvaluatorError( + command.instrType + ) + ); + } + await cmdEvaluators[instrType]( code, - command, + command as never, context, control, stash, @@ -325,21 +346,32 @@ export async function* generateCSEMachineStateStream( yield { stash, control, steps }; } } - -const cmdEvaluators: { [type: string]: CmdEvaluator } = { +function isDeclaredEvaluator(kind: string): kind is keyof CmdEvaluators { + return kind in cmdEvaluators; +} +type ExprKeys = Exclude; +type StmtKeys = Exclude; +type InstrKeys = Exclude; +type CmdEvaluators = { + [K in ExprKeys]: CmdEvaluator> +} & { + [K in StmtKeys]: CmdEvaluator> +} & { + [K in InstrKeys]: CmdEvaluator>; +}; +const cmdEvaluators: CmdEvaluators = { /** * AST Nodes */ FileInput: function ( _code: string, - command: ControlItem, + fileInput: StmtNS.FileInput, context: Context, control: Control, _stash: Stash, isPrelude: boolean, ) { - const node = command as StmtNS.FileInput; // Clean up non-global, non-program, and non-preparation environments while ( currentEnvironment(context).name !== "global" && @@ -350,40 +382,38 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { } // Create a new program environment if needed. - if (node.statements.length > 0 && currentEnvironment(context).name !== "programEnvironment") { + if (fileInput.statements.length > 0 && currentEnvironment(context).name !== "programEnvironment") { const programEnv = createProgramEnvironment(context, isPrelude); pushEnvironment(context, programEnv); } // Push the block body as a sequence of statements onto the control stack - const seq = node.statements.slice().reverse(); + const seq = fileInput.statements.slice().reverse(); control.push(...seq); }, SimpleExpr: function ( _code: string, - command: ControlItem, + simpleExpr: StmtNS.SimpleExpr, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const simpleExpr = command as StmtNS.SimpleExpr; // Discard the evaluated expression off the stack, // since Python statements are not value producing - control.push(instrCreator.popInstr(command as Node)); + control.push(instrCreator.popInstr(simpleExpr)); control.push(simpleExpr.expression); }, Literal: function ( _code: string, - command: ControlItem, + literal: ExprNS.Literal, _context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const literal = command as ExprNS.Literal; if (typeof literal.value === "number") { stash.push({ type: "number", value: literal.value }); } else if (typeof literal.value === "boolean") { @@ -397,25 +427,23 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { BigIntLiteral: function ( _code: string, - command: ControlItem, + bigIntLiteral: ExprNS.BigIntLiteral, _context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const literal = command as ExprNS.BigIntLiteral; - stash.push({ type: "bigint", value: BigInt(literal.value) }); + stash.push({ type: "bigint", value: BigInt(bigIntLiteral.value) }); }, Unary: function ( _code: string, - command: ControlItem, + unary: ExprNS.Unary, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const unary = command as ExprNS.Unary; const op_instr = instrCreator.unOpInstr(unary.operator.type, unary); control.push(op_instr); control.push(unary.right); @@ -423,13 +451,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { Binary: function ( _code: string, - command: ControlItem, + binary: ExprNS.Binary, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const binary = command as ExprNS.Binary; const op_instr = instrCreator.binOpInstr(binary.operator.type, binary); control.push(op_instr); control.push(binary.right); @@ -438,44 +465,41 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { BoolOp: function ( _code: string, - command: ControlItem, + boolOp: ExprNS.BoolOp, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const boolOp = command as ExprNS.BoolOp; control.push(instrCreator.boolOpInstr(boolOp.operator.type, boolOp)); control.push(boolOp.left); }, Grouping: function ( _code: string, - command: ControlItem, + grouping: ExprNS.Grouping, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const groupingNode = command as ExprNS.Grouping; - control.push(groupingNode.expression); + control.push(grouping.expression); }, Complex: function ( _code: string, - command: ControlItem, + complex: ExprNS.Complex, _context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const complexNode = command as ExprNS.Complex; - stash.push({ type: "complex", value: complexNode.value }); + stash.push({ type: "complex", value: complex.value }); }, None: function ( _code: string, - _command: ControlItem, + _command: ExprNS.None, _context: Context, _control: Control, stash: Stash, @@ -486,29 +510,27 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { Variable: function ( code: string, - command: ControlItem, + variable: ExprNS.Variable, context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const variableNode = command as ExprNS.Variable; - const name = variableNode.name.lexeme; + const name = variable.name.lexeme; // if not built in, look up in environment - const value = pyGetVariable(code, context, name, variableNode); + const value = pyGetVariable(code, context, name, variable); stash.push(value); }, Compare: function ( _code: string, - command: ControlItem, + compareNode: ExprNS.Compare, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const compareNode = command as ExprNS.Compare; const op_instr = instrCreator.binOpInstr(compareNode.operator.type, compareNode); control.push(op_instr); @@ -518,14 +540,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { Assign: function ( _code: string, - command: ControlItem, + assignNode: StmtNS.Assign, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const assignNode = command as StmtNS.Assign; - if (assignNode.target instanceof ExprNS.Subscript) { control.push(instrCreator.listAssmtInstr(assignNode.target)); control.push(assignNode.value); @@ -547,14 +567,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { Call: function ( _code: string, - command: ControlItem, + callNode: ExprNS.Call, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const callNode = command as ExprNS.Call; - const spreadIndices = callNode.args.reduce((acc, arg, i) => { if (arg instanceof ExprNS.Starred) acc.push(i); return acc; @@ -572,13 +590,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { FunctionDef: function ( _code: string, - command: ControlItem, + functionDefNode: StmtNS.FunctionDef, context: Context, _control: Control, _stash: Stash, _isPrelude: boolean, ) { - const functionDefNode = command as StmtNS.FunctionDef; const localVariables = scanForAssignments(functionDefNode.body); const closure = Closure.makeFromFunctionDef( functionDefNode, @@ -591,13 +608,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { Lambda: function ( _code: string, - command: ControlItem, + lambdaNode: ExprNS.Lambda, context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const lambdaNode = command as ExprNS.Lambda; const localVariables = scanForAssignments(lambdaNode.body); const closure = Closure.makeFromLambda( lambdaNode, @@ -610,13 +626,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { Return: function ( _code: string, - command: ControlItem, + returnNode: StmtNS.Return, _context: Context, control: Control, stash: Stash, _isPrelude: boolean, ) { - const returnNode = command as StmtNS.Return; let head; while (true) { head = control.pop(); @@ -637,7 +652,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { For: function ( _code: string, - _command: ControlItem, + _command: StmtNS.For, _context: Context, _control: Control, _stash: Stash, @@ -654,13 +669,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { While: function ( _code: string, - command: ControlItem, + whileNode: StmtNS.While, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const whileNode = command as StmtNS.While; const instr = instrCreator.whileInstr(whileNode, whileNode.condition, { kind: "StatementSequence", body: whileNode.body, @@ -671,35 +685,34 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { Break: function ( _code: string, - command: ControlItem, + command: StmtNS.Break, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - control.push(instrCreator.breakInstr(command as StmtNS.Break)); + control.push(instrCreator.breakInstr(command)); }, Continue: function ( _code: string, - command: ControlItem, + command: StmtNS.Continue, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - control.push(instrCreator.continueInstr(command as StmtNS.Continue)); + control.push(instrCreator.continueInstr(command)); }, If: function ( _code: string, - command: ControlItem, + ifNode: StmtNS.If, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const ifNode = command as StmtNS.If; const branch = instrCreator.branchInstr( { kind: "StatementSequence", body: ifNode.body }, ifNode.elseBlock @@ -718,30 +731,29 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { List: function ( _code: string, - command: ControlItem, + command: ExprNS.List, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { control.push( - instrCreator.listInstr((command as ExprNS.List).elements.length, command as ExprNS.List), + instrCreator.listInstr(command.elements.length, command), ); - for (let i = (command as ExprNS.List).elements.length - 1; i >= 0; i--) { - control.push((command as ExprNS.List).elements[i]); + for (let i = command.elements.length - 1; i >= 0; i--) { + control.push(command.elements[i]); } }, Subscript: function ( _code: string, - command: ControlItem, + subscriptNode: ExprNS.Subscript, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const subscriptNode = command as ExprNS.Subscript; control.push(instrCreator.listAccessInstr(subscriptNode)); control.push(subscriptNode.index); control.push(subscriptNode.value); @@ -749,13 +761,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { Ternary: function ( _code: string, - command: ControlItem, + ternaryNode: ExprNS.Ternary, _context: Context, control: Control, _stash: Stash, _isPrelude: boolean, ) { - const ternaryNode = command as ExprNS.Ternary; const branch = instrCreator.branchInstr( ternaryNode.consequent, ternaryNode.alternative, @@ -781,7 +792,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { */ [InstrType.RESET]: function ( _code: string, - _command: ControlItem, + _command: ResetInstr, context: Context, _control: Control, _stash: Stash, @@ -792,13 +803,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.ASSIGNMENT]: function ( code: string, - command: ControlItem, + instr: AssmtInstr, context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as AssmtInstr; const value = stash.pop(); if (value) { @@ -814,13 +824,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.LIST_ASSIGNMENT]: function ( code: string, - command: ControlItem, + instr: ListAssmtInstr, context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as ListAssmtInstr; const value = stash.pop(); const index = stash.pop(); const list = stash.pop(); @@ -830,7 +839,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.BREAK]: function ( _code: string, - command: ControlItem, + command: BreakInstr, _context: Context, control: Control, _stash: Stash, @@ -849,7 +858,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.CONTINUE]: function ( _code: string, - command: ControlItem, + command: ContinueInstr, _context: Context, control: Control, _stash: Stash, @@ -873,13 +882,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.UNARY_OP]: function ( code: string, - command: ControlItem, + instr: UnOpInstr, context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as UnOpInstr; const argument = stash.pop(); if (argument) { const result = evaluateUnaryExpression( @@ -895,14 +903,13 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.BINARY_OP]: function ( code: string, - command: ControlItem, + instr: BinOpInstr, context: Context, _control: Control, stash: Stash, _isPrelude: boolean, variant: number, ) { - const instr = command as BinOpInstr; const right = stash.pop(); const left = stash.pop(); if (left && right && variant) { @@ -921,13 +928,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.BOOL_OP]: function ( code: string, - command: ControlItem, + instr: BoolOpInstr, context: Context, control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as BoolOpInstr; const left = stash.pop(); const boolOpNode = instr.srcNode as ExprNS.BoolOp; const right = boolOpNode.right; @@ -960,7 +966,7 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.POP]: function ( _code: string, - _command: ControlItem, + _command: PopInstr, _context: Context, _control: Control, stash: Stash, @@ -971,13 +977,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.LIST]: function ( _code: string, - command: ControlItem, + instr: ListInstr, _context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as ListInstr; const elements: Value[] = []; for (let i = 0; i < instr.numOfElements; i++) { const element = stash.pop(); @@ -990,13 +995,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.WHILE]: function ( _code: string, - command: ControlItem, + instr: WhileInstr, _context: Context, control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as WhileInstr; const condition = stash.pop(); if (condition && !isFalsy(condition)) { control.push(instr); @@ -1006,13 +1010,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { }, [InstrType.APPLICATION]: async function ( code: string, - command: ControlItem, + instr: AppInstr, context: Context, control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as AppInstr; const numOfArgs = instr.numOfArgs; const rawArgs: Value[] = []; @@ -1085,13 +1088,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.LIST_ACCESS]: function ( code: string, - command: ControlItem, + instr: ListAccessInstr, context: Context, _control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as ListAccessInstr; const index = stash.pop(); const list = stash.pop(); if (!list || (list.type !== "list" && list.type !== "string")) { @@ -1135,13 +1137,12 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.BRANCH]: function ( _code: string, - command: ControlItem, + instr: BranchInstr, _context: Context, control: Control, stash: Stash, _isPrelude: boolean, ) { - const instr = command as BranchInstr; const condition = stash.pop(); if (condition && !isFalsy(condition)) { @@ -1163,20 +1164,20 @@ const cmdEvaluators: { [type: string]: CmdEvaluator } = { [InstrType.ENVIRONMENT]: function ( _code: string, - command: ControlItem, + command: EnvInstr, context: Context, _control: Control, _stash: Stash, _isPrelude: boolean, ) { - while (currentEnvironment(context).id !== (command as EnvInstr).env.id) { + while (currentEnvironment(context).id !== command.env.id) { popEnvironment(context); } }, [InstrType.END_OF_FUNCTION_BODY]: function ( _code: string, - _command: ControlItem, + _command: EndOfFunctionBodyInstr, _context: Context, _control: Control, stash: Stash, diff --git a/src/engines/cse/types.ts b/src/engines/cse/types.ts index 0bb612be..181c57a5 100644 --- a/src/engines/cse/types.ts +++ b/src/engines/cse/types.ts @@ -19,36 +19,17 @@ export enum InstrType { WHILE = "WhileInstr", FOR = "ForInstr", ASSIGNMENT = "Assignment", - ANN_ASSIGNMENT = "AnnAssignment", LIST_ASSIGNMENT = "ListAssignment", APPLICATION = "Application", UNARY_OP = "UnaryOperation", BINARY_OP = "BinaryOperation", BOOL_OP = "BoolOperation", - COMPARE = "Compare", - CALL = "Call", - RETURN = "Return", BREAK = "BreakInstr", CONTINUE = "ContinueInstr", - IF = "If", - FUNCTION_DEF = "FunctionDef", - LAMBDA = "Lambda", LIST = "ListLiteral", - MULTI_LAMBDA = "MultiLambda", - GROUPING = "Grouping", - LITERAL = "Literal", - VARIABLE = "Variable", - TERNARY = "Ternary", - PASS = "Pass", - ASSERT = "Assert", - IMPORT = "Import", - GLOBAL = "Global", - NONLOCAL = "NonLocal", - Program = "Program", BRANCH = "Branch", POP = "Pop", ENVIRONMENT = "environment", - MARKER = "marker", END_OF_FUNCTION_BODY = "EndOfFunctionBody", LIST_ACCESS = "ListAccess", } @@ -129,10 +110,6 @@ export interface EnvInstr extends BaseInstr { env: Environment; } -export interface ArrLitInstr extends BaseInstr { - arity: number; -} - export interface EndOfFunctionBodyInstr extends BaseInstr { instrType: InstrType.END_OF_FUNCTION_BODY; } @@ -151,7 +128,6 @@ export interface BoolOpInstr extends BaseInstr { } export type Instr = - | BaseInstr | WhileInstr | ForInstr | AssmtInstr @@ -160,11 +136,15 @@ export type Instr = | AppInstr | BranchInstr | EnvInstr - | ArrLitInstr | EndOfFunctionBodyInstr | ResetInstr | PopInstr - | BoolOpInstr; + | BoolOpInstr + | ListAccessInstr + | ListInstr + | ListAssmtInstr + | BreakInstr + | ContinueInstr; export function typeTranslator(type: Value["type"]): string { switch (type) { diff --git a/src/engines/cse/utils.ts b/src/engines/cse/utils.ts index 00da7962..9a5b8c7c 100644 --- a/src/engines/cse/utils.ts +++ b/src/engines/cse/utils.ts @@ -181,7 +181,6 @@ const propertySetter: PropertySetter = new Map([ [InstrType.BINARY_OP, setToFalse], [InstrType.BOOL_OP, setToFalse], [InstrType.POP, setToFalse], - [InstrType.MARKER, setToFalse], [InstrType.ASSIGNMENT, setToFalse], [InstrType.ENVIRONMENT, setToFalse], [InstrType.APPLICATION, setToFalse], diff --git a/src/stdlib/parser.ts b/src/stdlib/parser.ts index 96f75a75..27cee546 100644 --- a/src/stdlib/parser.ts +++ b/src/stdlib/parser.ts @@ -1,38 +1,19 @@ import { ExprNS, FunctionParam, StmtNS } from "../ast-types"; import { Context } from "../engines/cse/context"; -<<<<<<< Updated upstream import { handleRuntimeError } from "../engines/cse/error"; import { -======= -import { ControlItem } from "../engines/cse/control"; -import { handleRuntimeError } from "../engines/cse/error"; -import { - BigIntValue, - BoolValue, ->>>>>>> Stashed changes BuiltinValue, - ComplexValue, ListValue, NoneValue, -<<<<<<< Updated upstream StringValue, Value -======= - NumberValue, - StringValue, - Value, ->>>>>>> Stashed changes } from "../engines/cse/stash"; import { operatorTranslator } from "../engines/cse/types"; import { TypeError } from "../errors/errors"; import { parse } from "../parser"; import pythonLexer from "../parser/lexer"; import { minArgMap, Validate } from "../stdlib"; -<<<<<<< Updated upstream import { GroupName } from "./utils"; -======= -import { Group, GroupName } from "./utils"; ->>>>>>> Stashed changes const None: NoneValue = { type: "none" }; diff --git a/src/tests/parser-stdlib.test.ts b/src/tests/parser-stdlib.test.ts index bc619ce2..a71d7eec 100644 --- a/src/tests/parser-stdlib.test.ts +++ b/src/tests/parser-stdlib.test.ts @@ -1,10 +1,10 @@ +import { TypeError } from "../errors"; import linkedList from "../stdlib/linked-list"; import list from "../stdlib/list"; import pairmutator from "../stdlib/pairmutator"; -import stream from "../stdlib/stream"; import parser from "../stdlib/parser"; +import stream from "../stdlib/stream"; import { generateTestCases, TestCases } from "./utils"; -import { TypeError } from "../errors"; const groups = [linkedList, list, pairmutator, stream, parser]; @@ -239,7 +239,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f(x):\\n return x\\n"))', null, [ - "[ 'function_definition',\n[ ['name', ['f', None]],\n[ [['name', ['x', None]], None],\n[['return_statement', [['name', ['x', None]], None]], None]]]]", + "[ 'function_declaration',\n[ ['name', ['f', None]],\n[ [['name', ['x', None]], None],\n[['return_statement', [['name', ['x', None]], None]], None]]]]", ], ], ], @@ -248,7 +248,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f(x, y):\\n return x\\n"))', null, [ - "[ 'function_definition',\n[ ['name', ['f', None]],\n[ [['name', ['x', None]], [['name', ['y', None]], None]],\n[['return_statement', [['name', ['x', None]], None]], None]]]]", + "[ 'function_declaration',\n[ ['name', ['f', None]],\n[ [['name', ['x', None]], [['name', ['y', None]], None]],\n[['return_statement', [['name', ['x', None]], None]], None]]]]", ], ], ], @@ -257,7 +257,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f():\\n return 1\\n"))', null, [ - "[ 'function_definition',\n[ ['name', ['f', None]],\n[None, [['return_statement', [['literal', [1, None]], None]], None]]]]", + "[ 'function_declaration',\n[ ['name', ['f', None]],\n[None, [['return_statement', [['literal', [1, None]], None]], None]]]]", ], ], ], @@ -266,7 +266,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f(*args):\\n return args\\n"))', null, [ - "[ 'function_definition',\n[ ['name', ['f', None]],\n[ [['rest_element', [['name', ['args', None]], None]], None],\n[['return_statement', [['name', ['args', None]], None]], None]]]]", + "[ 'function_declaration',\n[ ['name', ['f', None]],\n[ [['rest_element', [['name', ['args', None]], None]], None],\n[['return_statement', [['name', ['args', None]], None]], None]]]]", ], ], ], @@ -275,7 +275,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f():\\n x = 1\\n return x\\n"))', null, [ - "[ 'function_definition',\n[ ['name', ['f', None]],\n[ None,\n[ [ 'block',\n [ [ 'sequence',\n [ [ ['declaration', [['name', ['x', None]], [['literal', [1, None]], None]]],\n [['return_statement', [['name', ['x', None]], None]], None]],\n None]],\n None]],\nNone]]]]", + "[ 'function_declaration',\n[ ['name', ['f', None]],\n[ None,\n[ [ 'block',\n [ [ 'sequence',\n [ [ ['declaration', [['name', ['x', None]], [['literal', [1, None]], None]]],\n [['return_statement', [['name', ['x', None]], None]], None]],\n None]],\n None]],\nNone]]]]", ], ], ], @@ -390,7 +390,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f(a):\\n pass\\nx = [1]\\nf(*x)"))', null, [ - "[ 'sequence',\n[ [ [ 'function_definition',\n [ ['name', ['f', None]],\n [[['name', ['a', None]], None], [['pass_statement', None], None]]]],\n [ [ 'declaration',\n [ ['name', ['x', None]],\n [['array_expression', [[['literal', [1, None]], None], None]], None]]],\n [ [ 'application',\n [ ['name', ['f', None]],\n [[['starred_expression', [['name', ['x', None]], None]], None], None]]],\n None]]],\nNone]]", + "[ 'sequence',\n[ [ [ 'function_declaration',\n [ ['name', ['f', None]],\n [[['name', ['a', None]], None], [['pass_statement', None], None]]]],\n [ [ 'declaration',\n [ ['name', ['x', None]],\n [['array_expression', [[['literal', [1, None]], None], None]], None]]],\n [ [ 'application',\n [ ['name', ['f', None]],\n [[['starred_expression', [['name', ['x', None]], None]], None], None]]],\n None]]],\nNone]]", ], ], ], @@ -404,7 +404,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f():\\n return 1\\n"))', null, [ - "[ 'function_definition',\n[ ['name', ['f', None]],\n[None, [['return_statement', [['literal', [1, None]], None]], None]]]]", + "[ 'function_declaration',\n[ ['name', ['f', None]],\n[None, [['return_statement', [['literal', [1, None]], None]], None]]]]", ], ], ], @@ -413,7 +413,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f():\\n return\\n"))', null, [ - "[ 'function_definition',\n[['name', ['f', None]], [None, [['return_statement', [None, None]], None]]]]", + "[ 'function_declaration',\n[['name', ['f', None]], [None, [['return_statement', [None, None]], None]]]]", ], ], ], @@ -426,7 +426,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f():\\n global x\\n"))', null, [ - "[ 'function_definition',\n[['name', ['f', None]], [None, [['global_statement', ['x', None]], None]]]]", + "[ 'function_declaration',\n[['name', ['f', None]], [None, [['global_statement', ['x', None]], None]]]]", ], ], ], @@ -435,7 +435,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f():\\n x = 1\\n def g():\\n nonlocal x\\n"))', null, [ - "[ 'function_definition',\n[ ['name', ['f', None]],\n[ None,\n[ [ 'block',\n [ [ 'sequence',\n [ [ ['declaration', [['name', ['x', None]], [['literal', [1, None]], None]]],\n [ [ 'function_definition',\n [['name', ['g', None]], [None, [['nonlocal_statement', ['x', None]], None]]]],\n None]],\n None]],\n None]],\nNone]]]]", + "[ 'function_declaration',\n[ ['name', ['f', None]],\n[ None,\n[ [ 'block',\n [ [ 'sequence',\n [ [ ['declaration', [['name', ['x', None]], [['literal', [1, None]], None]]],\n [ [ 'function_declaration',\n [['name', ['g', None]], [None, [['nonlocal_statement', ['x', None]], None]]]],\n None]],\n None]],\n None]],\nNone]]]]", ], ], ], @@ -527,7 +527,7 @@ describe("Parser Stdlib Tests", () => { 'print(parse("def f():\\n x = 1\\n y = 2\\n return x\\n"))', null, [ - "[ 'function_definition',\n[ ['name', ['f', None]],\n[ None,\n[ [ 'block',\n [ [ 'sequence',\n [ [ ['declaration', [['name', ['x', None]], [['literal', [1, None]], None]]],\n [ ['declaration', [['name', ['y', None]], [['literal', [2, None]], None]]],\n [['return_statement', [['name', ['x', None]], None]], None]]],\n None]],\n None]],\nNone]]]]", + "[ 'function_declaration',\n[ ['name', ['f', None]],\n[ None,\n[ [ 'block',\n [ [ 'sequence',\n [ [ ['declaration', [['name', ['x', None]], [['literal', [1, None]], None]]],\n [ ['declaration', [['name', ['y', None]], [['literal', [2, None]], None]]],\n [['return_statement', [['name', ['x', None]], None]], None]]],\n None]],\n None]],\nNone]]]]", ], ], ], From 152076b9e0265bb3bed8335be8f6626744f7594f Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Tue, 5 May 2026 21:13:27 +0800 Subject: [PATCH 14/19] Fix bug regarding missing py_s1_constants.json --- .../plugins/autocomplete/highlight-rules.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/conductor/plugins/autocomplete/highlight-rules.ts b/src/conductor/plugins/autocomplete/highlight-rules.ts index 040c82c3..91b816d5 100644 --- a/src/conductor/plugins/autocomplete/highlight-rules.ts +++ b/src/conductor/plugins/autocomplete/highlight-rules.ts @@ -29,16 +29,21 @@ */ import { AceRules } from "@sourceacademy/autocomplete"; -import constants from "../../../stdlib/py_s1_constants.json"; +import math from "../../../stdlib/math"; +import misc from "../../../stdlib/misc"; import { getIllegalKeywords, getKeywords } from "./keywords"; export default (variant: number) => { const keywords = getKeywords(variant).join("|"); const illegalKeywords = getIllegalKeywords(variant).join("|"); + const stdlibBuiltins = new Map([...math.builtins, ...misc.builtins]); + const builtinConstants = [...stdlibBuiltins.keys()] + .filter(x => stdlibBuiltins.get(x)?.type !== "builtin") + .join("|"); - const builtinConstants = constants.constants.join("|"); - - const builtinFunctions = constants.builtInFuncs.join("|"); + const builtinFunctions = [...stdlibBuiltins.keys()] + .filter(x => stdlibBuiltins.get(x)?.type === "builtin") + .join("|"); //var futureReserved = ""; const keywordMapper = { From 6194145b70acc01ff1aaad558383656a299b19f7 Mon Sep 17 00:00:00 2001 From: MengJit Date: Wed, 6 May 2026 20:49:06 +0800 Subject: [PATCH 15/19] feat(conductor): add per-evaluator autocomplete and syntax highlighting Each PyCseEvaluator variant registers its own autocomplete plugin with the correct variant number and evaluator name, so the runner emits a unique ace mode id (ace/mode/PyCseEvaluator{1..4}) per bundle. --- scripts/autocomplete.sh | 2 +- src/conductor/PyCseEvaluator.ts | 18 ++++++++++++------ src/conductor/plugins/autocomplete/index.ts | 6 +++++- src/conductor/plugins/autocomplete/mode.ts | 6 +++--- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/scripts/autocomplete.sh b/scripts/autocomplete.sh index 33d09277..5d93838a 100755 --- a/scripts/autocomplete.sh +++ b/scripts/autocomplete.sh @@ -14,7 +14,7 @@ mkdir -p "src/conductor/plugins/autocomplete/builtins" # outputting the AST as JSON to the src/conductor/plugins/autocomplete/builtins directory. for file in "$LIB"/*.js; do echo "Processing $file..." - $JSDOC -X -c "$CONF" "$file" > "src/conductor/plugins/autocomplete/builtins/$(basename "$file" .js).json" & + "$JSDOC" -X -c "$CONF" "$file" > "src/conductor/plugins/autocomplete/builtins/$(basename "$file" .js).json" & done wait diff --git a/src/conductor/PyCseEvaluator.ts b/src/conductor/PyCseEvaluator.ts index 19390789..35436a73 100644 --- a/src/conductor/PyCseEvaluator.ts +++ b/src/conductor/PyCseEvaluator.ts @@ -35,12 +35,18 @@ abstract class PyCseEvaluatorBase extends BasicEvaluator { private readonly variant: number; private readonly groups: Group[]; private readonly ensurePreludesLoaded: () => Promise; + - protected constructor(conductor: IRunnerPlugin, variant: number, groups: Group[]) { + protected constructor( + conductor: IRunnerPlugin, + variant: number, + groups: Group[], + evaluatorName: string + ) { super(conductor); this.variant = variant; this.groups = groups; - conductor.registerPlugin(AutoCompletePlugin, variant); + conductor.registerPlugin(AutoCompletePlugin, variant, evaluatorName); for (const group of this.groups) { for (const [name, value] of group.builtins) { this.context.nativeStorage.builtins.set(name, value); @@ -112,24 +118,24 @@ abstract class PyCseEvaluatorBase extends BasicEvaluator { export class PyCseEvaluator1 extends PyCseEvaluatorBase { constructor(conductor: IRunnerPlugin) { - super(conductor, 1, [misc, math]); + super(conductor, 1, [misc, math], "PyCseEvaluator1"); } } export class PyCseEvaluator2 extends PyCseEvaluatorBase { constructor(conductor: IRunnerPlugin) { - super(conductor, 2, [misc, math, linkedList]); + super(conductor, 2, [misc, math, linkedList], "PyCseEvaluator2"); } } export class PyCseEvaluator3 extends PyCseEvaluatorBase { constructor(conductor: IRunnerPlugin) { - super(conductor, 3, [misc, math, linkedList, list, pairmutator, stream]); + super(conductor, 3, [misc, math, linkedList, list, pairmutator, stream], "PyCseEvaluator3"); } } export class PyCseEvaluator4 extends PyCseEvaluatorBase { constructor(conductor: IRunnerPlugin) { - super(conductor, 4, [misc, math, linkedList, list, pairmutator, stream, parser]); + super(conductor, 4, [misc, math, linkedList, list, pairmutator, stream, parser], "PyCseEvaluator4"); } } diff --git a/src/conductor/plugins/autocomplete/index.ts b/src/conductor/plugins/autocomplete/index.ts index 30396f9d..a48c35ab 100644 --- a/src/conductor/plugins/autocomplete/index.ts +++ b/src/conductor/plugins/autocomplete/index.ts @@ -10,18 +10,22 @@ import { getNames } from "./resolver"; export default class AutoCompletePlugin extends BaseAutoCompleteRunnerPlugin { private readonly variant: number; + private readonly evaluatorName: string; + constructor( _conduit: IConduit, // eslint-disable-next-line @typescript-eslint/no-explicit-any channels: IChannel[], variant: number, + evaluatorName: string, ) { super(_conduit, channels); this.variant = variant; + this.evaluatorName = evaluatorName; } get mode(): SyntaxHighlightData { - return pythonMode(this.variant); + return pythonMode(this.variant, this.evaluatorName); } autocomplete(code: string, row: number, column: number): AutoCompleteEntry[] { diff --git a/src/conductor/plugins/autocomplete/mode.ts b/src/conductor/plugins/autocomplete/mode.ts index 31b3c583..3ff53653 100644 --- a/src/conductor/plugins/autocomplete/mode.ts +++ b/src/conductor/plugins/autocomplete/mode.ts @@ -1,6 +1,6 @@ import { SyntaxHighlightData } from "@sourceacademy/autocomplete"; import PythonHighlightRules from "./highlight-rules"; -export default (variant: number): SyntaxHighlightData => ({ +export default (variant: number, evaluatorName: string): SyntaxHighlightData => ({ highlightRules: PythonHighlightRules(variant), foldingRules: { hookFrom: "ace/mode/folding/pythonic", @@ -20,6 +20,6 @@ export default (variant: number): SyntaxHighlightData => ({ autoOutdent: { hookFrom: "ace/mode/python", }, - id: "ace/mode/python" + variant, - snippetFileId: "ace/snippets/python" + variant, + id: `ace/mode/${evaluatorName}`, + snippetFileId: `ace/snippets/${evaluatorName}`, }); From 376926f54e8d066c7c694e0ec4325a6d174c19ef Mon Sep 17 00:00:00 2001 From: MengJit Date: Wed, 6 May 2026 20:56:59 +0800 Subject: [PATCH 16/19] style: format PyCseEvaluator.ts --- src/conductor/PyCseEvaluator.ts | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/conductor/PyCseEvaluator.ts b/src/conductor/PyCseEvaluator.ts index 35436a73..ce3589fb 100644 --- a/src/conductor/PyCseEvaluator.ts +++ b/src/conductor/PyCseEvaluator.ts @@ -35,13 +35,12 @@ abstract class PyCseEvaluatorBase extends BasicEvaluator { private readonly variant: number; private readonly groups: Group[]; private readonly ensurePreludesLoaded: () => Promise; - protected constructor( - conductor: IRunnerPlugin, - variant: number, + conductor: IRunnerPlugin, + variant: number, groups: Group[], - evaluatorName: string + evaluatorName: string, ) { super(conductor); this.variant = variant; @@ -136,6 +135,11 @@ export class PyCseEvaluator3 extends PyCseEvaluatorBase { export class PyCseEvaluator4 extends PyCseEvaluatorBase { constructor(conductor: IRunnerPlugin) { - super(conductor, 4, [misc, math, linkedList, list, pairmutator, stream, parser], "PyCseEvaluator4"); + super( + conductor, + 4, + [misc, math, linkedList, list, pairmutator, stream, parser], + "PyCseEvaluator4", + ); } } From 1d73766b838846eb1355daa2d7db156bc74184dd Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Sat, 9 May 2026 15:37:16 +0800 Subject: [PATCH 17/19] Add `range` support + Add test cases Co-authored-by: Copilot --- .../plugins/autocomplete/highlight-rules.ts | 9 +- .../plugins/autocomplete/keywords.ts | 4 +- .../plugins/autocomplete/resolver.ts | 12 ++ src/tests/autocomplete.test.ts | 135 ++++++++++++++++++ 4 files changed, 154 insertions(+), 6 deletions(-) create mode 100644 src/tests/autocomplete.test.ts diff --git a/src/conductor/plugins/autocomplete/highlight-rules.ts b/src/conductor/plugins/autocomplete/highlight-rules.ts index 91b816d5..c3bbb37b 100644 --- a/src/conductor/plugins/autocomplete/highlight-rules.ts +++ b/src/conductor/plugins/autocomplete/highlight-rules.ts @@ -41,16 +41,17 @@ export default (variant: number) => { .filter(x => stdlibBuiltins.get(x)?.type !== "builtin") .join("|"); - const builtinFunctions = [...stdlibBuiltins.keys()] + let builtinFunctions = [...stdlibBuiltins.keys()] .filter(x => stdlibBuiltins.get(x)?.type === "builtin") .join("|"); + if (variant >= 3) { + builtinFunctions += "|range"; + } //var futureReserved = ""; const keywordMapper = { map: { - "invalid.deprecated": "debugger", "support.function": builtinFunctions, - "variable.language": "self|cls", "constant.language": builtinConstants, keyword: keywords, "invalid.illegal": illegalKeywords, @@ -118,7 +119,7 @@ export default (variant: number) => { }, { token: ["keyword", "text", "entity.name.function"], - regex: "(def|class)(\\s+)([\\u00BF-\\u1FFF\\u2C00-\\uD7FF\\w]+)", + regex: "def(\\s+)([\\u00BF-\\u1FFF\\u2C00-\\uD7FF\\w]+)", }, { token: "text", diff --git a/src/conductor/plugins/autocomplete/keywords.ts b/src/conductor/plugins/autocomplete/keywords.ts index 9ab990f4..c801d3ec 100644 --- a/src/conductor/plugins/autocomplete/keywords.ts +++ b/src/conductor/plugins/autocomplete/keywords.ts @@ -17,7 +17,7 @@ export const getKeywords = (variant: number): string[] => { ]; if (variant >= 3) { - keywords = keywords.concat(["while", "for", "break", "continue"]); + keywords = keywords.concat(["while", "for", "in", "break", "continue"]); } return keywords; }; @@ -42,7 +42,7 @@ export const getIllegalKeywords = (variant: number): string[] => { ]; if (variant < 3) { - illegalKeywords = illegalKeywords.concat(["while", "for", "break", "continue"]); + illegalKeywords = illegalKeywords.concat(["while", "for", "break", "continue", "in"]); } return illegalKeywords; }; diff --git a/src/conductor/plugins/autocomplete/resolver.ts b/src/conductor/plugins/autocomplete/resolver.ts index c781ffb8..e3ae081e 100644 --- a/src/conductor/plugins/autocomplete/resolver.ts +++ b/src/conductor/plugins/autocomplete/resolver.ts @@ -195,6 +195,7 @@ export const getNames = ( meta: CompletionItemKind.Keyword, score: score, // Keywords are given the highest score })) + .filter(s => isSubsequence(query, s.name)) .forEach(s => entries.push(s)); // TODO: Add docstrings for user-defined functions to autocomplete suggestions? @@ -203,10 +204,21 @@ export const getNames = ( symbols.push(...linkedListJSON); } if (variant >= 3) { + symbols.push({ + name: "range", + title: "range(start, [stop], [step]) -> range", + description: + "PRIMITIVE\nUsed with the for statement to create a loop that iterates a specific number of times. If given one argument, it iterates from 0 to that number (exclusive). If given two arguments, it iterates from the first (inclusive) to the second (exclusive). If given three arguments, it iterates from the first to the second in steps of the third.", + meta: "func", + }); symbols.push(...listJSON); symbols.push(...pairmutatorJSON); symbols.push(...streamJSON); } + // TODO: add when MCE documentation is pushed into main + // if (variant >= 4) { + // symbols.push(...mceJSON); + // } symbols .map(v => ({ name: v.name, diff --git a/src/tests/autocomplete.test.ts b/src/tests/autocomplete.test.ts new file mode 100644 index 00000000..5a7a0295 --- /dev/null +++ b/src/tests/autocomplete.test.ts @@ -0,0 +1,135 @@ +import { parser } from "@lezer/python"; +import { CompletionItemKind } from "@sourceacademy/autocomplete"; +import { getNames } from "../../src/conductor/plugins/autocomplete/resolver"; +const testContains = ( + code: string, + expected: { name: string; meta: CompletionItemKind }, + line: number, + column: number, + variant: number, +) => { + const tree = parser.parse(code); + const suggestions = getNames(tree, code, line, column, variant); + expect(suggestions).toContainEqual(expect.objectContaining(expected)); +}; + +const testNotContains = ( + code: string, + expected: { name: string; meta: CompletionItemKind }, + line: number, + column: number, + variant: number, +) => { + const tree = parser.parse(code); + const suggestions = getNames(tree, code, line, column, variant); + expect(suggestions).not.toContainEqual(expect.objectContaining(expected)); +}; +describe("Chapter 1 Autocomplete", () => { + test("should suggest built-in functions", () => { + testContains("le", { name: "len", meta: CompletionItemKind.Function }, 1, 2, 1); + }); + test("should suggest keywords", () => { + testContains("de", { name: "def", meta: CompletionItemKind.Keyword }, 1, 2, 1); + }); + test("can handle no suggestions", () => { + const tree = parser.parse("x = 10\nx."); + const suggestions = getNames(tree, "x = 10\nx.", 2, 3, 1); + expect(suggestions).toEqual([]); + }); + test("should suggest variables in scope", () => { + testContains( + "x = 10\ny = x + 5\nzab = y * 2\nza", + { name: "zab", meta: CompletionItemKind.Variable }, + 4, + 2, + 1, + ); + }); + test("can handle layers of scope", () => { + testContains( + "x = 10\ndef foo():\n y = x + 5\n def bar():\n zab = y * 2\n za", + { name: "zab", meta: CompletionItemKind.Variable }, + 6, + 10, + 1, + ); + testContains( + "x = 10\ndef foo():\n y = x + 5\n def bar():\n zab = y * 2\n y", + { name: "y", meta: CompletionItemKind.Variable }, + 6, + 9, + 1, + ); + testContains( + "x = 10\ndef foo():\n y = x + 5\n def bar():\n zab = y * 2\n x", + { name: "x", meta: CompletionItemKind.Variable }, + 6, + 9, + 1, + ); + + testNotContains( + "x = 10\ndef foo():\n y = x + 5\n def bar():\n zab = y * 2\nza", + { name: "zab", meta: CompletionItemKind.Variable }, + 6, + 2, + 1, + ); + testNotContains( + "x = 10\ndef foo():\n y = x + 5\n def bar():\n zab = y * 2\ny", + { name: "y", meta: CompletionItemKind.Variable }, + 6, + 1, + 1, + ); + testContains( + "x = 10\ndef foo():\n y = x + 5\n def bar():\n zab = y * 2\nx", + { name: "x", meta: CompletionItemKind.Variable }, + 6, + 1, + 1, + ); + }); +}); + +describe("Chapter 3 Autocomplete", () => { + test("while loops internals should not be visible", () => { + testNotContains( + "x = 10\nwhile x > 0:\n y = x + 5\n x -= 1\n zab = y * 2\nza", + { name: "y", meta: CompletionItemKind.Variable }, + 6, + 2, + 1, + ); + testNotContains( + "x = 10\nwhile x > 0:\n y = x + 5\n x -= 1\n zab = y * 2\ny", + { name: "zab", meta: CompletionItemKind.Variable }, + 6, + 1, + 1, + ); + }); + test("for loop internals should not be visible", () => { + testNotContains( + "x = 10\nfor i in range(x):\n y = i + 5\n zab = y * 2\nza", + { name: "zab", meta: CompletionItemKind.Variable }, + 5, + 2, + 1, + ); + testNotContains( + "x = 10\nfor i in range(x):\n y = i + 5\n zab = y * 2\ny", + { name: "y", meta: CompletionItemKind.Variable }, + 5, + 1, + 1, + ); + testNotContains( + "x = 10\nfor i in range(x):\n y = i + 5\n zab = y * 2\ni", + { name: "i", meta: CompletionItemKind.Variable }, + 5, + 1, + 1, + ); + }); +}); From f57b25c4bf6eca637d10ac9c1931f4db8ebb8dd0 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Sat, 9 May 2026 15:44:01 +0800 Subject: [PATCH 18/19] Update chapter 3 tests to use variant 3 --- src/tests/autocomplete.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/tests/autocomplete.test.ts b/src/tests/autocomplete.test.ts index 5a7a0295..2f7fedd2 100644 --- a/src/tests/autocomplete.test.ts +++ b/src/tests/autocomplete.test.ts @@ -99,14 +99,14 @@ describe("Chapter 3 Autocomplete", () => { { name: "y", meta: CompletionItemKind.Variable }, 6, 2, - 1, + 3, ); testNotContains( "x = 10\nwhile x > 0:\n y = x + 5\n x -= 1\n zab = y * 2\ny", { name: "zab", meta: CompletionItemKind.Variable }, 6, 1, - 1, + 3, ); }); test("for loop internals should not be visible", () => { @@ -115,21 +115,21 @@ describe("Chapter 3 Autocomplete", () => { { name: "zab", meta: CompletionItemKind.Variable }, 5, 2, - 1, + 3, ); testNotContains( "x = 10\nfor i in range(x):\n y = i + 5\n zab = y * 2\ny", { name: "y", meta: CompletionItemKind.Variable }, 5, 1, - 1, + 3, ); testNotContains( "x = 10\nfor i in range(x):\n y = i + 5\n zab = y * 2\ni", { name: "i", meta: CompletionItemKind.Variable }, 5, 1, - 1, + 3, ); }); }); From 7d590486072faaea82caa7a9b92a17b8de1eae83 Mon Sep 17 00:00:00 2001 From: Aarav Malani Date: Sat, 9 May 2026 16:07:03 +0800 Subject: [PATCH 19/19] Add more chapter-specific test cases --- .../plugins/autocomplete/highlight-rules.ts | 2 +- .../plugins/autocomplete/resolver.ts | 9 +++- src/tests/autocomplete.test.ts | 49 ++++++++++++++++++- 3 files changed, 57 insertions(+), 3 deletions(-) diff --git a/src/conductor/plugins/autocomplete/highlight-rules.ts b/src/conductor/plugins/autocomplete/highlight-rules.ts index c3bbb37b..0e6a9a5f 100644 --- a/src/conductor/plugins/autocomplete/highlight-rules.ts +++ b/src/conductor/plugins/autocomplete/highlight-rules.ts @@ -119,7 +119,7 @@ export default (variant: number) => { }, { token: ["keyword", "text", "entity.name.function"], - regex: "def(\\s+)([\\u00BF-\\u1FFF\\u2C00-\\uD7FF\\w]+)", + regex: "(def)(\\s+)([\\u00BF-\\u1FFF\\u2C00-\\uD7FF\\w]+)", }, { token: "text", diff --git a/src/conductor/plugins/autocomplete/resolver.ts b/src/conductor/plugins/autocomplete/resolver.ts index e3ae081e..6a7ea240 100644 --- a/src/conductor/plugins/autocomplete/resolver.ts +++ b/src/conductor/plugins/autocomplete/resolver.ts @@ -42,9 +42,12 @@ const extractEnvironment = (iter: TreeCursor, pos: number, doc: string): Environ let currentEnv = topEnv; do { if (iter.node.type.name === "ParamList") { + // If the position is inside a function parameter list, + // we don't want to suggest any names return null; } if (iter.node.type.name === "ForStatement") { + // Add loop variable to current environment const target = iter.node.getChild("VariableName"); if (target) { currentEnv.variables.push(getNodeText(target, doc)); @@ -95,6 +98,8 @@ const extractEnvironment = (iter: TreeCursor, pos: number, doc: string): Environ currentEnv.child = nextEnv; currentEnv = nextEnv; } while (iter.enter(pos, -1)); + + // If the node is a function name or loop variable, we don't want to include it in the autocomplete suggestions since it's not in scope at the cursor position. We check for this case by looking at the parent node of the current position - if it's a FunctionDefinition or ForStatement, we return null to indicate that no environment should be extracted. if ( iter.node.parent && ["FunctionDefinition", "ForStatement"].includes(iter.node.parent.type.name) @@ -171,7 +176,9 @@ export const getNames = ( const query = getNodeText(node, doc); let env: Environment | null = extractEnvironment(tree.cursor(), pos, doc); - + if (env === null) { + return []; + } const entries: AutoCompleteEntry[] = []; let score = 1; // The score is used to prioritize suggestions from inner scopes over outer scopes. Built-ins will have the lowest score. while (env) { diff --git a/src/tests/autocomplete.test.ts b/src/tests/autocomplete.test.ts index 2f7fedd2..ae5c95cc 100644 --- a/src/tests/autocomplete.test.ts +++ b/src/tests/autocomplete.test.ts @@ -28,6 +28,16 @@ describe("Chapter 1 Autocomplete", () => { test("should suggest built-in functions", () => { testContains("le", { name: "len", meta: CompletionItemKind.Function }, 1, 2, 1); }); + test("should not suggest built-ins when not a subsequence", () => { + testNotContains("el", { name: "len", meta: CompletionItemKind.Function }, 1, 2, 1); + }); + test("should not suggest Chapter 3 keywords", () => { + testNotContains("wh", { name: "while", meta: CompletionItemKind.Keyword }, 1, 2, 1); + testNotContains("fo", { name: "for", meta: CompletionItemKind.Keyword }, 1, 2, 1); + testNotContains("br", { name: "break", meta: CompletionItemKind.Keyword }, 1, 2, 1); + testNotContains("co", { name: "continue", meta: CompletionItemKind.Keyword }, 1, 2, 1); + testNotContains("i", { name: "in", meta: CompletionItemKind.Keyword }, 1, 1, 1); + }); test("should suggest keywords", () => { testContains("de", { name: "def", meta: CompletionItemKind.Keyword }, 1, 2, 1); }); @@ -83,13 +93,33 @@ describe("Chapter 1 Autocomplete", () => { 1, ); testContains( - "x = 10\ndef foo():\n y = x + 5\n def bar():\n zab = y * 2\nx", + "x = 10\ndef foo(x):\n y = x + 5\n def bar():\n zab = y * 2\nx", { name: "x", meta: CompletionItemKind.Variable }, 6, 1, 1, ); }); + test("does not suggest name during function definition", () => { + testNotContains("foo = 3\ndef f", { name: "f", meta: CompletionItemKind.Function }, 2, 5, 1); + testNotContains("foo = 3\ndef f", { name: "foo", meta: CompletionItemKind.Variable }, 2, 5, 1); + testNotContains( + "bar = 3\ndef f(b", + { name: "bar", meta: CompletionItemKind.Variable }, + 2, + 7, + 1, + ); + }); + test("suggests name during function call", () => { + testContains( + "foo = 3\ndef f():\n pass\nf(fo", + { name: "foo", meta: CompletionItemKind.Variable }, + 4, + 4, + 1, + ); + }); }); describe("Chapter 3 Autocomplete", () => { @@ -109,6 +139,16 @@ describe("Chapter 3 Autocomplete", () => { 3, ); }); + test("for loops should have the loop variable visible inside the loop", () => { + testContains( + "x = 10\nfor i in range(x):\n y = i + 5\n zab = y * 2\n i", + { name: "i", meta: CompletionItemKind.Variable }, + 5, + 5, + 3, + ); + }); + test("for loop internals should not be visible", () => { testNotContains( "x = 10\nfor i in range(x):\n y = i + 5\n zab = y * 2\nza", @@ -132,4 +172,11 @@ describe("Chapter 3 Autocomplete", () => { 3, ); }); + test("should suggest Chapter 3 keywords", () => { + testContains("wh", { name: "while", meta: CompletionItemKind.Keyword }, 1, 2, 3); + testContains("fo", { name: "for", meta: CompletionItemKind.Keyword }, 1, 2, 3); + testContains("br", { name: "break", meta: CompletionItemKind.Keyword }, 1, 2, 3); + testContains("co", { name: "continue", meta: CompletionItemKind.Keyword }, 1, 2, 3); + testContains("i", { name: "in", meta: CompletionItemKind.Keyword }, 1, 1, 3); + }); });