jslike 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,9 +1,15 @@
1
1
  {
2
2
  "name": "jslike",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "description": "Production-ready JavaScript interpreter with full ES6+ support using Acorn parser",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./validator": "./src/validator/index.js",
10
+ "./editor/monaco": "./src/editor/monaco/index.js",
11
+ "./src/editor/wang-prism": "./src/editor/wang-prism.js"
12
+ },
7
13
  "bin": {
8
14
  "jslike": "./bin/jslike.js"
9
15
  },
@@ -0,0 +1,234 @@
1
+ // Wang Language Definition for Monaco Editor
2
+ export const wangLanguage = {
3
+ defaultToken: 'invalid',
4
+ tokenPostfix: '.wang',
5
+
6
+ keywords: [
7
+ 'let',
8
+ 'const',
9
+ 'var',
10
+ 'if',
11
+ 'else',
12
+ 'for',
13
+ 'while',
14
+ 'do',
15
+ 'break',
16
+ 'continue',
17
+ 'return',
18
+ 'function',
19
+ 'class',
20
+ 'extends',
21
+ 'constructor',
22
+ 'async',
23
+ 'await',
24
+ 'import',
25
+ 'export',
26
+ 'from',
27
+ 'as',
28
+ 'try',
29
+ 'catch',
30
+ 'finally',
31
+ 'throw',
32
+ 'this',
33
+ 'super',
34
+ 'new',
35
+ 'typeof',
36
+ 'instanceof',
37
+ 'in',
38
+ 'of',
39
+ ],
40
+
41
+ typeKeywords: [],
42
+
43
+ operators: [
44
+ '=>',
45
+ '+=',
46
+ '-=',
47
+ '*=',
48
+ '/=',
49
+ '===',
50
+ '!==',
51
+ '==',
52
+ '!=',
53
+ '<=',
54
+ '>=',
55
+ '<<',
56
+ '>>',
57
+ '>>>',
58
+ '&&',
59
+ '||',
60
+ '??',
61
+ '?.',
62
+ '...',
63
+ '++',
64
+ '--',
65
+ '**',
66
+ '+',
67
+ '-',
68
+ '*',
69
+ '/',
70
+ '%',
71
+ '&',
72
+ '|',
73
+ '^',
74
+ '~',
75
+ '!',
76
+ '?',
77
+ ':',
78
+ '=',
79
+ '<',
80
+ '>',
81
+ ],
82
+
83
+ symbols: /[=><!~?:&|+\-*\/\^%]+/,
84
+
85
+ escapes: /\\(?:[abfnrtv\\"']|x[0-9A-Fa-f]{1,4}|u[0-9A-Fa-f]{4}|U[0-9A-Fa-f]{8})/,
86
+
87
+ tokenizer: {
88
+ root: [
89
+ // Identifiers and keywords
90
+ [
91
+ /[a-zA-Z_$][\w$]*/,
92
+ {
93
+ cases: {
94
+ '@keywords': 'keyword',
95
+ 'true|false|null|undefined': 'constant',
96
+ '@default': 'identifier',
97
+ },
98
+ },
99
+ ],
100
+
101
+ // Whitespace
102
+ { include: '@whitespace' },
103
+
104
+ // Delimiters and operators
105
+ [/[{}()\[\]]/, '@brackets'],
106
+ [/[<>](?!@symbols)/, '@brackets'],
107
+ [
108
+ /@symbols/,
109
+ {
110
+ cases: {
111
+ '@operators': 'operator',
112
+ '@default': '',
113
+ },
114
+ },
115
+ ],
116
+
117
+ // Numbers
118
+ [/\d*\.\d+([eE][\-+]?\d+)?/, 'number.float'],
119
+ [/0[xX][0-9a-fA-F]+/, 'number.hex'],
120
+ [/\d+/, 'number'],
121
+
122
+ // Delimiter
123
+ [/[;,.]/, 'delimiter'],
124
+
125
+ // Strings
126
+ [/"([^"\\]|\\.)*$/, 'string.invalid'],
127
+ [/'([^'\\]|\\.)*$/, 'string.invalid'],
128
+ [/"/, 'string', '@string_double'],
129
+ [/'/, 'string', '@string_single'],
130
+
131
+ // Template literals
132
+ [/`/, 'string.template', '@template'],
133
+
134
+ // Regular expressions
135
+ [/\/(?=([^\/\\]|\\.)+\/)/, 'regexp', '@regexp'],
136
+ ],
137
+
138
+ whitespace: [
139
+ [/[ \t\r\n]+/, ''],
140
+ [/\/\*/, 'comment', '@comment'],
141
+ [/\/\/.*$/, 'comment'],
142
+ ],
143
+
144
+ comment: [
145
+ [/[^\/*]+/, 'comment'],
146
+ [/\*\//, 'comment', '@pop'],
147
+ [/[\/*]/, 'comment'],
148
+ ],
149
+
150
+ string_double: [
151
+ [/[^\\"]+/, 'string'],
152
+ [/@escapes/, 'string.escape'],
153
+ [/\\./, 'string.escape.invalid'],
154
+ [/"/, 'string', '@pop'],
155
+ ],
156
+
157
+ string_single: [
158
+ [/[^\\']+/, 'string'],
159
+ [/@escapes/, 'string.escape'],
160
+ [/\\./, 'string.escape.invalid'],
161
+ [/'/, 'string', '@pop'],
162
+ ],
163
+
164
+ template: [
165
+ [/[^`\\$]+/, 'string.template'],
166
+ [/@escapes/, 'string.escape'],
167
+ [/\\./, 'string.escape.invalid'],
168
+ [/\${/, { token: 'delimiter.bracket', next: '@templateExpression' }],
169
+ [/`/, 'string.template', '@pop'],
170
+ ],
171
+
172
+ templateExpression: [[/}/, { token: 'delimiter.bracket', next: '@pop' }], { include: 'root' }],
173
+
174
+ regexp: [
175
+ [
176
+ /(\{)(\d+(?:,\d*)?)(\})/,
177
+ ['regexp.escape.control', 'regexp.escape.control', 'regexp.escape.control'],
178
+ ],
179
+ [/(\[)([^\]\\]|\\.)*(\])/, 'regexp.escape.control'],
180
+ [/(\()(\?:|\?=|\?!)/, ['regexp.escape.control', 'regexp.escape.control']],
181
+ [/[()]/, 'regexp.escape.control'],
182
+ [/@escapes/, 'regexp.escape'],
183
+ [/\\./, 'regexp.escape'],
184
+ [/\/[gimsuy]*/, { token: 'regexp', bracket: '@close', next: '@pop' }],
185
+ [/./, 'regexp'],
186
+ ],
187
+ },
188
+ };
189
+
190
+ // Register the language with Monaco
191
+ export function registerWangLanguage(monaco) {
192
+ monaco.languages.register({ id: 'wang' });
193
+ monaco.languages.setMonarchTokensProvider('wang', wangLanguage);
194
+
195
+ // Configuration for brackets and auto-closing
196
+ monaco.languages.setLanguageConfiguration('wang', {
197
+ comments: {
198
+ lineComment: '//',
199
+ blockComment: ['/*', '*/'],
200
+ },
201
+ brackets: [
202
+ ['{', '}'],
203
+ ['[', ']'],
204
+ ['(', ')'],
205
+ ],
206
+ autoClosingPairs: [
207
+ { open: '{', close: '}' },
208
+ { open: '[', close: ']' },
209
+ { open: '(', close: ')' },
210
+ { open: '"', close: '"', notIn: ['string'] },
211
+ { open: "'", close: "'", notIn: ['string'] },
212
+ { open: '`', close: '`', notIn: ['string'] },
213
+ { open: '/*', close: '*/' },
214
+ ],
215
+ surroundingPairs: [
216
+ { open: '{', close: '}' },
217
+ { open: '[', close: ']' },
218
+ { open: '(', close: ')' },
219
+ { open: '"', close: '"' },
220
+ { open: "'", close: "'" },
221
+ { open: '`', close: '`' },
222
+ ],
223
+ folding: {
224
+ markers: {
225
+ start: new RegExp('^\\s*//\\s*#?region\\b'),
226
+ end: new RegExp('^\\s*//\\s*#?endregion\\b'),
227
+ },
228
+ },
229
+ indentationRules: {
230
+ increaseIndentPattern: new RegExp('^.*(\\{[^}]*|\\([^)]*|\\[[^\\]]*)$'),
231
+ decreaseIndentPattern: new RegExp('^\\s*(\\}|\\)|\\])'),
232
+ },
233
+ });
234
+ }
@@ -0,0 +1,136 @@
1
+ // Wang Language Definition for Prism.js
2
+ /* global Prism */
3
+ (function (Prism) {
4
+ Prism.languages.wang = {
5
+ // Comments
6
+ comment: [
7
+ {
8
+ pattern: /\/\*[\s\S]*?\*\//,
9
+ greedy: true,
10
+ },
11
+ {
12
+ pattern: /\/\/.*/,
13
+ greedy: true,
14
+ },
15
+ ],
16
+
17
+ // Template literals with interpolation
18
+ 'template-string': {
19
+ pattern: /`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,
20
+ greedy: true,
21
+ inside: {
22
+ 'template-punctuation': {
23
+ pattern: /^`|`$/,
24
+ alias: 'string',
25
+ },
26
+ interpolation: {
27
+ pattern: /((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,
28
+ lookbehind: true,
29
+ inside: {
30
+ 'interpolation-punctuation': {
31
+ pattern: /^\$\{|\}$/,
32
+ alias: 'punctuation',
33
+ },
34
+ rest: Prism.languages.wang,
35
+ },
36
+ },
37
+ string: /[\s\S]+/,
38
+ },
39
+ },
40
+
41
+ // Strings
42
+ string: [
43
+ {
44
+ pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,
45
+ greedy: true,
46
+ },
47
+ ],
48
+
49
+ // Regular expressions
50
+ regex: {
51
+ pattern: /\/(?:\\.|[^\/\\\r\n])+\/[gimsuy]*/,
52
+ greedy: true,
53
+ inside: {
54
+ 'regex-delimiter': {
55
+ pattern: /^\/|\/[gimsuy]*$/,
56
+ alias: 'punctuation',
57
+ },
58
+ 'regex-flags': {
59
+ pattern: /[gimsuy]+$/,
60
+ alias: 'keyword',
61
+ },
62
+ },
63
+ },
64
+
65
+ // Keywords
66
+ keyword:
67
+ /\b(?:let|const|var|if|else|for|while|do|break|continue|return|function|class|extends|constructor|async|await|import|export|from|as|try|catch|finally|throw|new|this|super|typeof|instanceof|in|of)\b/,
68
+
69
+ // Boolean and null values
70
+ boolean: /\b(?:true|false)\b/,
71
+ null: /\b(?:null|undefined)\b/,
72
+
73
+ // Numbers
74
+ number: /\b(?:0x[\dA-Fa-f]+|0b[01]+|0o[0-7]+|\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)\b/,
75
+
76
+ // Arrow function operator
77
+ 'arrow-operator': {
78
+ pattern: /=>/,
79
+ alias: 'operator',
80
+ },
81
+
82
+ // Operators (order matters for matching)
83
+ operator:
84
+ /\+\+|--|===|!==|==|!=|<=|>=|<<|>>>|>>|&&|\|\||[?]{2}|\?\.|\.{3}|\*\*|\+=|-=|\*=|\/=|[+\-*/%<>&|^~!?:]=?/,
85
+
86
+ // Punctuation
87
+ punctuation: /[{}[\];(),.:]/,
88
+
89
+ // Functions (pattern for function calls)
90
+ function: /\b[a-zA-Z_$][\w$]*(?=\s*\()/,
91
+
92
+ // Class names (after class or extends)
93
+ 'class-name': {
94
+ pattern: /(\b(?:class|extends)\s+)[a-zA-Z_$][\w$]*/,
95
+ lookbehind: true,
96
+ },
97
+
98
+ // Property names in object literals
99
+ property: {
100
+ pattern:
101
+ /((?:^|[,{])[ \t]*)(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*|"(?:\\.|[^\\"\r\n])*"|'(?:\\.|[^\\'\r\n])*')(?=\s*:)/m,
102
+ lookbehind: true,
103
+ greedy: true,
104
+ },
105
+
106
+ // Variables and identifiers (must come last)
107
+ variable: /\b[a-zA-Z_$][\w$]*/,
108
+ };
109
+
110
+ // Set wang as an alias for javascript-like highlighting
111
+ Prism.languages.wang['class-name'].pattern = /(\b(?:class|extends|new)\s+)[a-zA-Z_$][\w$]*/;
112
+
113
+ // Hook to handle special Wang syntax
114
+ Prism.hooks.add('before-tokenize', function (env) {
115
+ if (env.language !== 'wang') {
116
+ return;
117
+ }
118
+ });
119
+
120
+ // Hook to add special classes for Wang-specific features
121
+ Prism.hooks.add('wrap', function (env) {
122
+ if (env.language !== 'wang') {
123
+ return;
124
+ }
125
+
126
+ // Add special class for Wang keywords
127
+ if (env.type === 'keyword' && ['import', 'export', 'from', 'as'].includes(env.content)) {
128
+ env.classes.push('wang-module');
129
+ }
130
+ });
131
+ })(Prism);
132
+
133
+ // Export for module systems
134
+ if (typeof module !== 'undefined' && module.exports) {
135
+ module.exports = Prism.languages.wang;
136
+ }
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // Use bundled Acorn parser for zero runtime dependencies
2
2
  import { parse as acornParse } from './parser.js';
3
3
  import { Interpreter } from './interpreter/interpreter.js';
4
- import { Environment } from './runtime/environment.js';
4
+ import { Environment, ReturnValue } from './runtime/environment.js';
5
5
  import { createGlobalEnvironment } from './runtime/builtins.js';
6
6
 
7
7
  // Helper to detect if code contains module syntax or top-level await
@@ -231,7 +231,8 @@ export function parse(code, options = {}) {
231
231
  return acornParse(processedCode, {
232
232
  ecmaVersion: 2022, // Support ES2022 features (including top-level await)
233
233
  sourceType: sourceType,
234
- locations: false // Don't track source locations (faster)
234
+ locations: true, // Track source locations for better error messages
235
+ allowReturnOutsideFunction: true // Allow top-level return statements
235
236
  });
236
237
  } catch (error) {
237
238
  // Reformat error message for consistency
@@ -318,10 +319,10 @@ export async function execute(code, env = null, options = {}) {
318
319
 
319
320
  if (needsAsync) {
320
321
  const result = await interpreter.evaluateAsync(ast, userEnv);
321
- return result;
322
+ return result instanceof ReturnValue ? result.value : result;
322
323
  } else {
323
324
  const result = interpreter.evaluate(ast, userEnv);
324
- return result;
325
+ return result instanceof ReturnValue ? result.value : result;
325
326
  }
326
327
  }
327
328
 
@@ -335,3 +336,46 @@ export const isTopLevelAwait = containsModuleSyntax;
335
336
 
336
337
  export { Interpreter } from './interpreter/interpreter.js';
337
338
  export { Environment } from './runtime/environment.js';
339
+ export { WangInterpreter, InMemoryModuleResolver } from './interpreter/index.js';
340
+
341
+ /**
342
+ * Abstract base class for module resolution
343
+ * Extend this class to implement custom module loading strategies
344
+ */
345
+ export class ModuleResolver {
346
+ /**
347
+ * Resolve a module and return its code
348
+ * @param {string} modulePath - The module path to resolve
349
+ * @param {string} [fromPath] - The path of the importing module
350
+ * @returns {Promise<ModuleResolution>} The resolved module
351
+ */
352
+ async resolve(modulePath, fromPath) {
353
+ throw new Error('ModuleResolver.resolve() must be implemented by subclass');
354
+ }
355
+
356
+ /**
357
+ * Check if a module exists
358
+ * @param {string} modulePath - The module path to check
359
+ * @param {string} [fromPath] - The path of the importing module
360
+ * @returns {Promise<boolean>} Whether the module exists
361
+ */
362
+ async exists(modulePath, fromPath) {
363
+ throw new Error('ModuleResolver.exists() must be implemented by subclass');
364
+ }
365
+
366
+ /**
367
+ * List available modules
368
+ * @param {string} [prefix] - Optional prefix to filter modules
369
+ * @returns {Promise<string[]>} List of module paths
370
+ */
371
+ async list(prefix) {
372
+ throw new Error('ModuleResolver.list() must be implemented by subclass');
373
+ }
374
+ }
375
+
376
+ /**
377
+ * @typedef {Object} ModuleResolution
378
+ * @property {string} code - The module source code
379
+ * @property {string} path - The resolved module path
380
+ * @property {any} [metadata] - Optional metadata about the module
381
+ */
@@ -42,6 +42,13 @@ export class WangInterpreter {
42
42
  return this.setVariable(name, func);
43
43
  }
44
44
 
45
+ // Expose global context for accessing variables after execution
46
+ get globalContext() {
47
+ return {
48
+ variables: this.persistentEnv?.vars || new Map()
49
+ };
50
+ }
51
+
45
52
  createExecutionEnvironment() {
46
53
  // Create fresh environment for each execution
47
54
  const env = createEnvironment();
@@ -164,11 +171,31 @@ export class InMemoryModuleResolver {
164
171
  this.modules = new Map();
165
172
  }
166
173
 
167
- addModule(name, code) {
168
- this.modules.set(name, code);
174
+ addModule(name, code, metadata) {
175
+ this.modules.set(name, { code, metadata });
176
+ }
177
+
178
+ async resolve(modulePath, fromPath) {
179
+ const module = this.modules.get(modulePath);
180
+ if (!module) {
181
+ return null;
182
+ }
183
+ return {
184
+ code: typeof module === 'string' ? module : module.code,
185
+ path: modulePath,
186
+ metadata: module.metadata
187
+ };
169
188
  }
170
189
 
171
- async resolve(name) {
172
- return this.modules.get(name);
190
+ async exists(modulePath, fromPath) {
191
+ return this.modules.has(modulePath);
192
+ }
193
+
194
+ async list(prefix) {
195
+ const paths = Array.from(this.modules.keys());
196
+ if (prefix) {
197
+ return paths.filter(p => p.startsWith(prefix));
198
+ }
199
+ return paths;
173
200
  }
174
201
  }
@@ -75,6 +75,10 @@ export class Interpreter {
75
75
  let result = undefined;
76
76
  for (const statement of node.body) {
77
77
  result = await this.evaluateAsync(statement, env);
78
+ // Handle top-level return and throw
79
+ if (result instanceof ReturnValue || result instanceof ThrowSignal) {
80
+ return result;
81
+ }
78
82
  }
79
83
  return result;
80
84
  }
@@ -1199,11 +1203,14 @@ export class Interpreter {
1199
1203
  moduleExports = this.moduleCache.get(modulePath);
1200
1204
  } else {
1201
1205
  // Resolve and load module code
1202
- const moduleCode = await this.moduleResolver.resolve(modulePath);
1203
- if (!moduleCode) {
1206
+ const resolution = await this.moduleResolver.resolve(modulePath);
1207
+ if (!resolution) {
1204
1208
  throw new Error(`Cannot find module '${modulePath}'`);
1205
1209
  }
1206
1210
 
1211
+ // Handle both old (string) and new (ModuleResolution) formats
1212
+ const moduleCode = typeof resolution === 'string' ? resolution : resolution.code;
1213
+
1207
1214
  // Parse and execute module in its own environment
1208
1215
  const moduleAst = acornParse(moduleCode, {
1209
1216
  ecmaVersion: 2020,
@@ -0,0 +1,103 @@
1
+ /**
2
+ * WangValidator - Syntax validation for Wang/JSLike code
3
+ */
4
+
5
+ import { parse, preprocessCode } from '../index.js';
6
+
7
+ export class WangValidator {
8
+ /**
9
+ * Validate code and return validation result
10
+ * @param {string} code - The code to validate
11
+ * @param {object} options - Validation options
12
+ * @param {boolean} options.includeAST - Whether to include AST in result
13
+ * @returns {object} Validation result
14
+ */
15
+ validate(code, options = {}) {
16
+ try {
17
+ // Preprocess code (handles ASI, etc.)
18
+ const processed = preprocessCode(code);
19
+
20
+ // Parse the code
21
+ const ast = parse(processed);
22
+
23
+ return {
24
+ valid: true,
25
+ ast: options.includeAST ? ast : undefined
26
+ };
27
+ } catch (error) {
28
+ // Extract line and column from error
29
+ const line = error.loc?.line || 1;
30
+ const column = error.loc?.column || 0;
31
+
32
+ // Generate suggestion for common errors
33
+ const suggestion = this.getSuggestion(error, code);
34
+
35
+ return {
36
+ valid: false,
37
+ error: {
38
+ message: error.message,
39
+ line,
40
+ column,
41
+ suggestion
42
+ }
43
+ };
44
+ }
45
+ }
46
+
47
+ /**
48
+ * Generate suggestions for common syntax errors
49
+ * @param {Error} error - The parse error
50
+ * @param {string} code - The original code
51
+ * @returns {string|undefined} Suggestion text
52
+ */
53
+ getSuggestion(error, code) {
54
+ const message = error.message.toLowerCase();
55
+
56
+ // Common error patterns and suggestions
57
+ if (message.includes('unexpected token')) {
58
+ if (message.includes('}')) {
59
+ return 'Check for missing opening brace or extra closing brace';
60
+ }
61
+ if (message.includes('{')) {
62
+ return 'Check for missing closing brace or misplaced opening brace';
63
+ }
64
+ if (message.includes(')')) {
65
+ return 'Check for missing opening parenthesis or extra closing parenthesis';
66
+ }
67
+ if (message.includes('(')) {
68
+ return 'Check for missing closing parenthesis';
69
+ }
70
+ return 'Check syntax near the error location';
71
+ }
72
+
73
+ if (message.includes('unterminated string')) {
74
+ return 'Add closing quote to string literal';
75
+ }
76
+
77
+ if (message.includes('unterminated template')) {
78
+ return 'Add closing backtick to template literal';
79
+ }
80
+
81
+ if (message.includes('identifier expected')) {
82
+ return 'Provide a valid identifier name';
83
+ }
84
+
85
+ if (message.includes('unexpected end of input')) {
86
+ return 'Check for unclosed brackets, braces, or parentheses';
87
+ }
88
+
89
+ if (message.includes('invalid left-hand side')) {
90
+ return 'Check that assignment target is a valid variable or property';
91
+ }
92
+
93
+ if (message.includes('duplicate parameter')) {
94
+ return 'Remove duplicate parameter name in function definition';
95
+ }
96
+
97
+ if (message.includes('strict mode')) {
98
+ return 'This syntax is not allowed in strict mode';
99
+ }
100
+
101
+ return undefined;
102
+ }
103
+ }