Skip to content

Commit

Permalink
Merge branch 'handle-class-skulpt' into lib
Browse files Browse the repository at this point in the history
  • Loading branch information
SebastienTainon committed Aug 21, 2024
2 parents 0d1fca5 + 02ddeaf commit 5a40282
Show file tree
Hide file tree
Showing 15 changed files with 397 additions and 151 deletions.
2 changes: 1 addition & 1 deletion frontend/buffers/Editor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -511,7 +511,7 @@ export function Editor(props: EditorProps) {
for (let {block, pos} of Object.values(blocksToInsert.elements)) {
let insertNewLineBefore = false;
let insertNewLineAfter = false;
if ((BlockType.Function === block.type && block.category !== 'sensors') || BlockType.Directive === block.type) {
if (((BlockType.Function === block.type || BlockType.ClassFunction === block.type) && block.category !== 'sensors') || BlockType.Directive === block.type) {
insertNewLineBefore = insertNewLineAfter = true;
}
if (BlockType.Token === block.type && block.snippet && -1 !== block.snippet.indexOf('${')) {
Expand Down
2 changes: 2 additions & 0 deletions frontend/buffers/editorAutocompletion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export const addAutocompletion = function (blocks: Block[], strings: any) {

switch (block.type) {
case BlockType.Function:
case BlockType.ClassFunction:
completions.push({
caption: block.caption,
snippet: block.snippet,
Expand All @@ -25,6 +26,7 @@ export const addAutocompletion = function (blocks: Block[], strings: any) {
});
break;
case BlockType.Constant:
case BlockType.ClassConstant:
let name = block.name;
if (strings.constant && strings.constant[name]) {
name = strings.constant[name];
Expand Down
2 changes: 1 addition & 1 deletion frontend/stepper/analysis/AnalysisFunctionLocals.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export const AnalysisFunctionLocals = (props: AnalysisFunctionLocalsProps): JSX.

const variablesTemplate = variables.map((variable) => {
const {name, value, type} = variable;
if (value !== undefined && 'module' !== type && 'function' !== type) {
if (value !== undefined && 'module' !== type && 'function' !== type && 'type' !== type) {
return (
<li key={name}>
<AnalysisVariable
Expand Down
7 changes: 3 additions & 4 deletions frontend/stepper/analysis/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export const convertVariableDAPToCodecastFormat = (stackFrameId: number, scopeId
let previousValue = previousValueHash in previousValues ? previousValues[previousValueHash] : null;

let variables = null;
if (variable.variables) {
if (Array.isArray(variable.variables)) {
const innerVariables = [];
for (let innerVariable of variable.variables) {
if (typeof innerVariable === 'string') {
Expand All @@ -131,9 +131,8 @@ export const convertVariableDAPToCodecastFormat = (stackFrameId: number, scopeId
innerVariables.push(result);
}
}
if (innerVariables.length) {
variables = innerVariables;
}

variables = innerVariables;
}

return {
Expand Down
3 changes: 1 addition & 2 deletions frontend/stepper/c/unix_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ import {CodecastPlatform} from '../codecast_platform';
import {Block, BlockType} from '../../task/blocks/block_types';
import {getContextBlocksDataSelector} from '../../task/blocks/blocks';
import {quickAlgoLibraries} from '../../task/libs/quick_algo_libraries_model';
import {convertSkulptStateToAnalysisSnapshot} from '../python/analysis';
import {convertAnalysisDAPToCodecastFormat} from '../analysis/analysis';
import {Codecast} from '../../app_types';
import {parseDirectives} from '../python/directives';

const RETURN_TYPE_CONVERSION = {
Expand Down Expand Up @@ -83,6 +81,7 @@ export default class UnixRunner extends AbstractRunner {
const argsSection = block.params.map(param => {
return param in PARAM_TYPE_CONVERSION ? PARAM_TYPE_CONVERSION[param] : 'int';
}).join(', ');
// @ts-ignore
headers[code] = `${block.returnType in RETURN_TYPE_CONVERSION ? RETURN_TYPE_CONVERSION[block.returnType] : 'void'} ${code}(${argsSection});`;
}

Expand Down
5 changes: 5 additions & 0 deletions frontend/stepper/python/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ export const analyseSkulptScope = function(suspension: any): AnalysisScope {
continue;
}

// This is an object of a class exported by a module, we don't want to display it in analysis
if ('object' === typeof value && value.__variableName) {
continue;
}

if (typeof value === 'function') {
if (!value.prototype || !value.prototype.tp$name) {
continue;
Expand Down
206 changes: 158 additions & 48 deletions frontend/stepper/python/python_runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {parseDirectives} from './directives';
import {convertAnalysisDAPToCodecastFormat} from '../analysis/analysis';
import {appSelect} from '../../hooks';
import {quickAlgoLibraries} from '../../task/libs/quick_algo_libraries_model';
import {getContextBlocksDataSelector} from '../../task/blocks/blocks';
import {CONSTRUCTOR_NAME, getContextBlocksDataSelector} from '../../task/blocks/blocks';
import {delay} from '../../player/sagas';
import {put} from 'typed-redux-saga';
import {LibraryTestResult} from '../../task/libs/library_test_result';
Expand Down Expand Up @@ -43,6 +43,8 @@ function definePythonNumber() {
return pythonNumber;
}

const PROXY_IDENTITY = Symbol('proxy_target_identity');

const PythonNumber = definePythonNumber();

export default class PythonRunner extends AbstractRunner {
Expand Down Expand Up @@ -92,37 +94,89 @@ export default class PythonRunner extends AbstractRunner {
log.getLogger('python_runner').debug('AFTER FINAL INTERACT');
}

private static _skulptifyHandler(name, generatorName, blockName, nbArgs, type) {
let handler = '';
handler += "\tCodecast.runner.checkArgs('" + name + "', '" + generatorName + "', '" + blockName + "', arguments);";
handler += "\tCodecast.runner.hasCalledHandler = true;";
private static _skulptifyHandler(name, generatorName, blockName, nbArgs, type, toExecute, moduleMethodName?: string) {
return `
mod.${moduleMethodName ?? name} = new Sk.builtin.func(function () {
Codecast.runner.checkArgs('${name}', '${generatorName}', '${blockName}', arguments);
Codecast.runner.hasCalledHandler = true;
var susp = new Sk.misceval.Suspension();
var result = Sk.builtin.none.none$;
var args = Array.prototype.slice.call(arguments);
for (var i=0; i<args.length; i++) {
args[i] = Codecast.runner.skToJs(args[i]);
}
susp.resume = function() {
return result;
};
susp.data = {
type: 'Sk.promise',
promise: new Promise(function (resolve) {
${'actions' === type ? `Codecast.runner._nbActions += 1;` : ''}
try {
const result = Codecast.runner.quickAlgoCallsExecutor("${generatorName}", "${toExecute}", args);
if (result instanceof Promise) {
result.then(resolve).catch((e) => { Codecast.runner._onStepError(e) })
}
} catch (e) {
Codecast.runner._onStepError(e)
}
}).then(function (value) {
result = value;
return value;
})
};
return susp;
});
`;
}

handler += "\n\tvar susp = new Sk.misceval.Suspension();";
handler += "\n\tvar result = Sk.builtin.none.none$;";
private static _skulptifyClassHandler(methodName, generatorName, blockName, nbArgs, type, className: string, moduleMethodName: string) {
const handler = PythonRunner._skulptifyHandler(methodName, generatorName, blockName, nbArgs, type, `${className}->${methodName}`, moduleMethodName);

// If there are arguments, convert them from Skulpt format to the libs format
handler += "\n\tvar args = Array.prototype.slice.call(arguments);";
handler += "\n\tfor(var i=0; i<args.length; i++) { args[i] = Codecast.runner.skToJs(args[i]); };";
return handler.replace(/mod\./, '$loc.');
}

private static _skulptifyClassInstance(classInstance: string, className: string) {
return `
mod.${classInstance} = Sk.misceval.callsimArray(mod.${className});
mod.${classInstance}.__variableName = '${classInstance}';
`;
}

handler += "\n\tsusp.resume = function() { return result; };";
handler += "\n\tsusp.data = {type: 'Sk.promise', promise: new Promise(function(resolve) {";
private static _skulptifyClass(className: string, classComponents: string[]) {
return `
newClass${className} = function ($gbl, $loc) {
${classComponents.join("")}
};
mod.${className} = Sk.misceval.buildClass(mod, newClass${className}, "${className}", []);
`;
}

// Count actions
if (type == 'actions') {
handler += "\n\tCodecast.runner._nbActions += 1;";
private static _skulptifyConst(name, value) {
let handler = '';
if (typeof value === "number") {
handler = 'Sk.builtin.int_(' + value + ');';
} else if (typeof value === "boolean") {
handler = 'Sk.builtin.bool(' + value.toString() + ');';
} else if (typeof value === "string") {
handler = 'Sk.builtin.str(' + JSON.stringify(value) + ');';
} else {
throw "Unable to translate value '" + value + "' into a Skulpt constant.";
}

handler += "\n\ttry {";
handler += '\n\t\tconst result = Codecast.runner.quickAlgoCallsExecutor("' + generatorName + '", "' + blockName + '", args);';
handler += '\n\t\tif (result instanceof Promise) {';
handler += '\n\t\t\tresult.then(resolve).catch((e) => { Codecast.runner._onStepError(e) })';
handler += '\n\t\t}';
handler += "\n\t} catch (e) {";
handler += "\n\t\tCodecast.runner._onStepError(e)}";
handler += '\n\t}).then(function (value) {\nresult = value;\nreturn value;\n })};';
handler += '\n\treturn susp;';
return '\nmod.' + name + ' = new ' + handler + '\n';
}

private static _skulptifyClassConstHandler(name, value) {
const handler = PythonRunner._skulptifyConst(name, value);

return '\nmod.' + name + ' = new Sk.builtin.func(function () {\n' + handler + '\n});\n';
return handler.replace(/mod\./, '$loc.');
}

private _createBuiltin(name, generatorName, blockName, nbArgs, type) {
Expand Down Expand Up @@ -163,21 +217,6 @@ export default class PythonRunner extends AbstractRunner {
}
}

private static _skulptifyConst(name, value) {
let handler = '';
if (typeof value === "number") {
handler = 'Sk.builtin.int_(' + value + ');';
} else if (typeof value === "boolean") {
handler = 'Sk.builtin.bool(' + value.toString() + ');';
} else if (typeof value === "string") {
handler = 'Sk.builtin.str(' + JSON.stringify(value) + ');';
} else {
throw "Unable to translate value '" + value + "' into a Skulpt constant.";
}

return '\nmod.' + name + ' = new ' + handler + '\n';
}

private _injectFunctions() {
// Generate Python custom libraries from all generated blocks
log.getLogger('python_runner').debug('inject functions', this.availableBlocks);
Expand All @@ -197,18 +236,59 @@ export default class PythonRunner extends AbstractRunner {

for (let block of blocks.filter(block => block.type === BlockType.Function)) {
const {code, generatorName, name, params, type} = block;
modContents += PythonRunner._skulptifyHandler(code, generatorName, name, params, type);
modContents += PythonRunner._skulptifyHandler(code, generatorName, name, params, type, name);
// We do want to override Python's naturel input and output to replace them with our own modules
if (generatorName === 'printer' && ('input' === code || 'print' === code)) {
Sk.builtins[code] = this._createBuiltin(code, generatorName, name, params, type);
}
}

const classInstancesToAdd: {[classInstance: string]: string} = {};
const classParts: {[className: string]: {[methodName: string]: string}} = {};
for (let block of blocks.filter(block => block.type === BlockType.ClassFunction)) {
const {generatorName, name, params, type, methodName, className, classInstance} = block;
if (!block.placeholderClassInstance) {
classInstancesToAdd[classInstance] = className;
}
if (!(className in classParts)) {
classParts[className] = {};
}
if (!(methodName in classParts[className])) {
let moduleMethodName = methodName;
if (CONSTRUCTOR_NAME === methodName) {
moduleMethodName = '__init__';
}
classParts[className][methodName] = PythonRunner._skulptifyClassHandler(methodName, generatorName, name, params, type, className, moduleMethodName);
}
}

for (let block of blocks.filter(block => block.type === BlockType.ClassConstant)) {
const {name, value, className, methodName} = block;
if (!(className in classParts)) {
classParts[className] = {};
}
if (!(name in classParts[className])) {
classParts[className][name] = PythonRunner._skulptifyClassConstHandler(methodName, value);
}
}

console.log({classParts})

for (let [className, classPartsList] of Object.entries(classParts)) {
modContents += PythonRunner._skulptifyClass(className, Object.values(classPartsList));
}

for (let [classInstance, className] of Object.entries(classInstancesToAdd)) {
modContents += PythonRunner._skulptifyClassInstance(classInstance, className);
}

for (let block of blocks.filter(block => block.type === BlockType.Constant)) {
const {name, value} = block;
modContents += PythonRunner._skulptifyConst(name, value);
}

console.log(modContents);

modContents += "\nreturn mod;\n};";
Sk.builtinFiles["files"]["src/lib/" + generatorName + ".js"] = modContents;
this.availableModules.push(generatorName);
Expand All @@ -224,14 +304,23 @@ export default class PythonRunner extends AbstractRunner {
console.error("Couldn't find the number of arguments for " + generatorName + "/" + blockName + ".");
return;
}

let argsGivenCount = args.length;
let params = block.paramsCount;
if (block.type === BlockType.ClassFunction) {
// The class handler received self as first argument
argsGivenCount--;
}

console.log('check args', block, params);

if (params.length === 0) {
// This function doesn't have arguments
if (args.length > 0) {
msg = name + "() takes no arguments (" + args.length + " given)";
if (argsGivenCount > 0) {
msg = name + "() takes no arguments (" + argsGivenCount + " given)";
throw new Sk.builtin.TypeError(msg);
}
} else if (params.indexOf(args.length) === -1 && params.indexOf(Infinity) === -1) {
} else if (params.indexOf(argsGivenCount) === -1 && params.indexOf(Infinity) === -1) {
let minArgs = params[0];
let maxArgs = params[0];
for (let i = 1; i < params.length; i++) {
Expand All @@ -241,17 +330,19 @@ export default class PythonRunner extends AbstractRunner {

if (minArgs === maxArgs) {
msg = name + "() takes exactly " + minArgs + " arguments";
} else if (args.length < minArgs) {
} else if (argsGivenCount < minArgs) {
msg = name + "() takes at least " + minArgs + " arguments";
} else if (args.length > maxArgs) {
} else if (argsGivenCount > maxArgs) {
msg = name + "() takes at most " + maxArgs + " arguments";
} else {
msg = name + "() doesn't have a variant accepting this number of arguments";
}
msg += " (" + args.length + " given)";
msg += " (" + argsGivenCount + " given)";

throw new Sk.builtin.TypeError(msg);
}

console.log('check successful');
}

skToJs (val) {
Expand Down Expand Up @@ -313,6 +404,25 @@ export default class PythonRunner extends AbstractRunner {
}

return retVal;
} else if (val instanceof Sk.builtin.object && val.hasOwnProperty('$d')) {
return new Proxy(val, {
get: (target, prop) => {
if (PROXY_IDENTITY === prop) {
return target
}

const value = target.$d.entries[prop];
console.log('getter', {target, prop, value})

return value && value.length ? this.skToJs(value[1]) : null;
},
set: (target, prop, value) => {
console.log('update', {target, prop, value});
Sk.abstr.objectSetItem(target['$d'], new Sk.builtin.str(prop), value[PROXY_IDENTITY] ?? this._createPrimitive(value));

return true;
},
});
} else {
let retVal = val.v;
if (val instanceof Sk.builtin.tuple || val instanceof Sk.builtin.list) {
Expand Down Expand Up @@ -429,7 +539,7 @@ export default class PythonRunner extends AbstractRunner {
}
}

public initCodes(codes, availableBlocks) {
public initCodes(codes, availableBlocks: Block[]) {
super.initCodes(codes, availableBlocks);

// For reportValue in Skulpt.
Expand Down
Loading

0 comments on commit 5a40282

Please sign in to comment.