hispano-lang 1.0.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/LICENSE +21 -0
- package/README.md +404 -0
- package/bin/hispano.js +313 -0
- package/dist/cli.d.ts +46 -0
- package/dist/evaluator.js +1391 -0
- package/dist/index.d.ts +185 -0
- package/dist/index.js +74 -0
- package/dist/interpreter.js +73 -0
- package/dist/parser.js +1050 -0
- package/dist/tokenizer.js +445 -0
- package/package.json +72 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tokenizer for HispanoLang
|
|
3
|
+
* Converts source code into tokens for the parser
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class Tokenizer {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.source = '';
|
|
9
|
+
this.tokens = [];
|
|
10
|
+
this.current = 0;
|
|
11
|
+
this.startPos = 0;
|
|
12
|
+
this.currentLine = 1;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Tokenizes source code and returns a list of tokens
|
|
17
|
+
* @param {string} source - Source code to tokenize
|
|
18
|
+
* @returns {Array} List of tokens
|
|
19
|
+
*/
|
|
20
|
+
tokenize(source) {
|
|
21
|
+
this.source = source;
|
|
22
|
+
this.tokens = [];
|
|
23
|
+
this.current = 0;
|
|
24
|
+
|
|
25
|
+
while (!this.isAtEnd()) {
|
|
26
|
+
this.scanToken();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
this.tokens.push({
|
|
30
|
+
type: 'EOF',
|
|
31
|
+
lexeme: '',
|
|
32
|
+
literal: null,
|
|
33
|
+
line: this.currentLine,
|
|
34
|
+
});
|
|
35
|
+
return this.tokens;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Scans the next token
|
|
40
|
+
*/
|
|
41
|
+
scanToken() {
|
|
42
|
+
this.startPos = this.current;
|
|
43
|
+
|
|
44
|
+
// Check for comments before advancing
|
|
45
|
+
if (
|
|
46
|
+
this.source[this.current] === '/' &&
|
|
47
|
+
this.source[this.current + 1] === '/'
|
|
48
|
+
) {
|
|
49
|
+
this.comment();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const char = this.advance();
|
|
54
|
+
|
|
55
|
+
switch (char) {
|
|
56
|
+
case ' ':
|
|
57
|
+
case '\r':
|
|
58
|
+
case '\t':
|
|
59
|
+
// Ignore whitespace
|
|
60
|
+
break;
|
|
61
|
+
|
|
62
|
+
case '\n':
|
|
63
|
+
this.currentLine++;
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case '=':
|
|
67
|
+
if (this.peek() === '=') {
|
|
68
|
+
this.advance();
|
|
69
|
+
this.addToken('EQUAL_EQUAL');
|
|
70
|
+
} else {
|
|
71
|
+
this.addToken('EQUAL');
|
|
72
|
+
}
|
|
73
|
+
break;
|
|
74
|
+
|
|
75
|
+
case '*':
|
|
76
|
+
if (this.peek() === '=') {
|
|
77
|
+
this.advance();
|
|
78
|
+
this.addToken('STAR_EQUAL');
|
|
79
|
+
} else {
|
|
80
|
+
this.addToken('STAR');
|
|
81
|
+
}
|
|
82
|
+
break;
|
|
83
|
+
|
|
84
|
+
case '/':
|
|
85
|
+
if (this.peek() === '=') {
|
|
86
|
+
this.advance();
|
|
87
|
+
this.addToken('SLASH_EQUAL');
|
|
88
|
+
} else {
|
|
89
|
+
this.addToken('SLASH');
|
|
90
|
+
}
|
|
91
|
+
break;
|
|
92
|
+
|
|
93
|
+
case '%':
|
|
94
|
+
if (this.peek() === '=') {
|
|
95
|
+
this.advance();
|
|
96
|
+
this.addToken('PERCENT_EQUAL');
|
|
97
|
+
} else {
|
|
98
|
+
this.addToken('PERCENT');
|
|
99
|
+
}
|
|
100
|
+
break;
|
|
101
|
+
|
|
102
|
+
case '>':
|
|
103
|
+
if (this.peek() === '=') {
|
|
104
|
+
this.advance();
|
|
105
|
+
this.addToken('GREATER_EQUAL');
|
|
106
|
+
} else {
|
|
107
|
+
this.addToken('GREATER');
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
|
|
111
|
+
case '<':
|
|
112
|
+
if (this.peek() === '=') {
|
|
113
|
+
this.advance();
|
|
114
|
+
this.addToken('LESS_EQUAL');
|
|
115
|
+
} else {
|
|
116
|
+
this.addToken('LESS');
|
|
117
|
+
}
|
|
118
|
+
break;
|
|
119
|
+
|
|
120
|
+
case '!':
|
|
121
|
+
if (this.peek() === '=') {
|
|
122
|
+
this.advance();
|
|
123
|
+
this.addToken('BANG_EQUAL');
|
|
124
|
+
} else {
|
|
125
|
+
this.addToken('BANG');
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
case '+':
|
|
130
|
+
if (this.peek() === '+') {
|
|
131
|
+
this.advance();
|
|
132
|
+
this.addToken('PLUS_PLUS');
|
|
133
|
+
} else if (this.peek() === '=') {
|
|
134
|
+
this.advance();
|
|
135
|
+
this.addToken('PLUS_EQUAL');
|
|
136
|
+
} else {
|
|
137
|
+
this.addToken('PLUS');
|
|
138
|
+
}
|
|
139
|
+
break;
|
|
140
|
+
|
|
141
|
+
case '-':
|
|
142
|
+
if (this.peek() === '-') {
|
|
143
|
+
this.advance();
|
|
144
|
+
this.addToken('MINUS_MINUS');
|
|
145
|
+
} else if (this.peek() === '=') {
|
|
146
|
+
this.advance();
|
|
147
|
+
this.addToken('MINUS_EQUAL');
|
|
148
|
+
} else {
|
|
149
|
+
this.addToken('MINUS');
|
|
150
|
+
}
|
|
151
|
+
break;
|
|
152
|
+
|
|
153
|
+
case '{':
|
|
154
|
+
this.addToken('LEFT_BRACE');
|
|
155
|
+
break;
|
|
156
|
+
|
|
157
|
+
case '}':
|
|
158
|
+
this.addToken('RIGHT_BRACE');
|
|
159
|
+
break;
|
|
160
|
+
|
|
161
|
+
case '(':
|
|
162
|
+
this.addToken('LEFT_PAREN');
|
|
163
|
+
break;
|
|
164
|
+
|
|
165
|
+
case ')':
|
|
166
|
+
this.addToken('RIGHT_PAREN');
|
|
167
|
+
break;
|
|
168
|
+
|
|
169
|
+
case ',':
|
|
170
|
+
this.addToken('COMMA');
|
|
171
|
+
break;
|
|
172
|
+
|
|
173
|
+
case ';':
|
|
174
|
+
this.addToken('SEMICOLON');
|
|
175
|
+
break;
|
|
176
|
+
|
|
177
|
+
case ':':
|
|
178
|
+
this.addToken('COLON');
|
|
179
|
+
break;
|
|
180
|
+
|
|
181
|
+
case '[':
|
|
182
|
+
this.addToken('LEFT_BRACKET');
|
|
183
|
+
break;
|
|
184
|
+
|
|
185
|
+
case ']':
|
|
186
|
+
this.addToken('RIGHT_BRACKET');
|
|
187
|
+
break;
|
|
188
|
+
|
|
189
|
+
case '.':
|
|
190
|
+
this.addToken('DOT');
|
|
191
|
+
break;
|
|
192
|
+
|
|
193
|
+
case '"':
|
|
194
|
+
this.string();
|
|
195
|
+
break;
|
|
196
|
+
|
|
197
|
+
case '/':
|
|
198
|
+
if (this.peek() === '/') {
|
|
199
|
+
this.comment();
|
|
200
|
+
} else {
|
|
201
|
+
this.addToken('SLASH');
|
|
202
|
+
}
|
|
203
|
+
break;
|
|
204
|
+
|
|
205
|
+
default:
|
|
206
|
+
if (this.isDigit(char)) {
|
|
207
|
+
this.number();
|
|
208
|
+
} else if (this.isAlpha(char)) {
|
|
209
|
+
this.identifier();
|
|
210
|
+
} else {
|
|
211
|
+
throw new Error(
|
|
212
|
+
`Unexpected character: ${char} at line ${this.currentLine}`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
break;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Processes a string literal
|
|
221
|
+
*/
|
|
222
|
+
string() {
|
|
223
|
+
while (this.peek() !== '"' && !this.isAtEnd()) {
|
|
224
|
+
if (this.peek() === '\n') this.currentLine++;
|
|
225
|
+
this.advance();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (this.isAtEnd()) {
|
|
229
|
+
throw new Error('Unterminated string');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Consume the closing quote
|
|
233
|
+
this.advance();
|
|
234
|
+
|
|
235
|
+
// Extract the string value
|
|
236
|
+
const value = this.source.substring(this.startPos + 1, this.current - 1);
|
|
237
|
+
this.addToken('STRING', value);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Scans a comment
|
|
242
|
+
*/
|
|
243
|
+
comment() {
|
|
244
|
+
// Consume the second '/'
|
|
245
|
+
this.advance();
|
|
246
|
+
|
|
247
|
+
// Skip until end of line
|
|
248
|
+
while (this.peek() !== '\n' && !this.isAtEnd()) {
|
|
249
|
+
this.advance();
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Don't add comment as a token - just skip it
|
|
253
|
+
// The comment is now fully consumed
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Processes a number
|
|
258
|
+
*/
|
|
259
|
+
number() {
|
|
260
|
+
while (this.isDigit(this.peek())) {
|
|
261
|
+
this.advance();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// Look for decimal part
|
|
265
|
+
if (this.peek() === '.' && this.isDigit(this.peekNext())) {
|
|
266
|
+
// Consume the dot
|
|
267
|
+
this.advance();
|
|
268
|
+
|
|
269
|
+
while (this.isDigit(this.peek())) {
|
|
270
|
+
this.advance();
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const value = this.source.substring(this.startPos, this.current);
|
|
275
|
+
this.addToken('NUMBER', parseFloat(value));
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Returns the next character without advancing
|
|
280
|
+
* @returns {string} Next character
|
|
281
|
+
*/
|
|
282
|
+
peekNext() {
|
|
283
|
+
if (this.current + 1 >= this.source.length) return '\0';
|
|
284
|
+
return this.source[this.current + 1];
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Processes an identifier or keyword
|
|
289
|
+
*/
|
|
290
|
+
identifier() {
|
|
291
|
+
while (this.isAlphaNumeric(this.peek())) {
|
|
292
|
+
this.advance();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const text = this.source.substring(this.startPos, this.current);
|
|
296
|
+
|
|
297
|
+
// Special handling for 'y' - only treat as AND in logical contexts
|
|
298
|
+
if (text === 'y') {
|
|
299
|
+
// Check if this is a logical context by looking at previous tokens
|
|
300
|
+
const prevToken = this.tokens[this.tokens.length - 1];
|
|
301
|
+
if (
|
|
302
|
+
prevToken &&
|
|
303
|
+
(prevToken.type === 'IDENTIFIER' ||
|
|
304
|
+
prevToken.type === 'NUMBER' ||
|
|
305
|
+
prevToken.type === 'STRING' ||
|
|
306
|
+
prevToken.type === 'TRUE' ||
|
|
307
|
+
prevToken.type === 'FALSE' ||
|
|
308
|
+
prevToken.type === 'RIGHT_PAREN' ||
|
|
309
|
+
prevToken.type === 'RIGHT_BRACKET')
|
|
310
|
+
) {
|
|
311
|
+
// Check if the next token is a logical operator or end of expression
|
|
312
|
+
const nextChar = this.peek();
|
|
313
|
+
if (
|
|
314
|
+
nextChar === ' ' ||
|
|
315
|
+
nextChar === '\n' ||
|
|
316
|
+
nextChar === '\t' ||
|
|
317
|
+
nextChar === ')' ||
|
|
318
|
+
nextChar === '}' ||
|
|
319
|
+
nextChar === ';' ||
|
|
320
|
+
this.isAtEnd()
|
|
321
|
+
) {
|
|
322
|
+
this.addToken('AND');
|
|
323
|
+
} else {
|
|
324
|
+
this.addToken('IDENTIFIER');
|
|
325
|
+
}
|
|
326
|
+
} else {
|
|
327
|
+
this.addToken('IDENTIFIER');
|
|
328
|
+
}
|
|
329
|
+
} else {
|
|
330
|
+
const type = this.getKeywordType(text);
|
|
331
|
+
this.addToken(type);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Determines if the identifier is a keyword
|
|
337
|
+
* @param {string} text - Identifier text
|
|
338
|
+
* @returns {string} Token type
|
|
339
|
+
*/
|
|
340
|
+
getKeywordType(text) {
|
|
341
|
+
const keywords = {
|
|
342
|
+
variable: 'VARIABLE',
|
|
343
|
+
mostrar: 'MOSTRAR',
|
|
344
|
+
leer: 'LEER',
|
|
345
|
+
si: 'SI',
|
|
346
|
+
sino: 'SINO',
|
|
347
|
+
mientras: 'MIENTRAS',
|
|
348
|
+
para: 'PARA',
|
|
349
|
+
funcion: 'FUNCION',
|
|
350
|
+
retornar: 'RETORNAR',
|
|
351
|
+
verdadero: 'TRUE',
|
|
352
|
+
falso: 'FALSE',
|
|
353
|
+
o: 'OR',
|
|
354
|
+
romper: 'ROMPER',
|
|
355
|
+
continuar: 'CONTINUAR',
|
|
356
|
+
intentar: 'INTENTAR',
|
|
357
|
+
capturar: 'CAPTURAR',
|
|
358
|
+
nulo: 'NULL',
|
|
359
|
+
indefinido: 'UNDEFINED',
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
return keywords[text] || 'IDENTIFIER';
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Advances the pointer and returns the previous character
|
|
367
|
+
* @returns {string} Previous character
|
|
368
|
+
*/
|
|
369
|
+
advance() {
|
|
370
|
+
this.current++;
|
|
371
|
+
return this.source[this.current - 1];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Marks the start of the current token
|
|
376
|
+
*/
|
|
377
|
+
get start() {
|
|
378
|
+
return this.current - 1;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Returns the current character without advancing
|
|
383
|
+
* @returns {string} Current character
|
|
384
|
+
*/
|
|
385
|
+
peek() {
|
|
386
|
+
if (this.isAtEnd()) return '\0';
|
|
387
|
+
return this.source[this.current];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Checks if we have reached the end of the code
|
|
392
|
+
* @returns {boolean} True if we reached the end
|
|
393
|
+
*/
|
|
394
|
+
isAtEnd() {
|
|
395
|
+
return this.current >= this.source.length;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Checks if a character is a digit
|
|
400
|
+
* @param {string} char - Character to check
|
|
401
|
+
* @returns {boolean} True if it is a digit
|
|
402
|
+
*/
|
|
403
|
+
isDigit(char) {
|
|
404
|
+
return char >= '0' && char <= '9';
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Checks if a character is alphabetic
|
|
409
|
+
* @param {string} char - Character to check
|
|
410
|
+
* @returns {boolean} True if it is alphabetic
|
|
411
|
+
*/
|
|
412
|
+
isAlpha(char) {
|
|
413
|
+
return (
|
|
414
|
+
(char >= 'a' && char <= 'z') ||
|
|
415
|
+
(char >= 'A' && char <= 'Z') ||
|
|
416
|
+
char === '_'
|
|
417
|
+
);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Checks if a character is alphanumeric
|
|
422
|
+
* @param {string} char - Character to check
|
|
423
|
+
* @returns {boolean} True if it is alphanumeric
|
|
424
|
+
*/
|
|
425
|
+
isAlphaNumeric(char) {
|
|
426
|
+
return this.isAlpha(char) || this.isDigit(char);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Adds a token to the list
|
|
431
|
+
* @param {string} type - Token type
|
|
432
|
+
* @param {any} literal - Literal value of the token
|
|
433
|
+
*/
|
|
434
|
+
addToken(type, literal = null) {
|
|
435
|
+
const text = this.source.substring(this.startPos, this.current);
|
|
436
|
+
this.tokens.push({
|
|
437
|
+
type,
|
|
438
|
+
lexeme: text,
|
|
439
|
+
literal,
|
|
440
|
+
line: this.currentLine,
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
module.exports = Tokenizer;
|
package/package.json
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "hispano-lang",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Un lenguaje de programación educativo en español para enseñar programación sin barreras de idioma",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"hispano": "./bin/hispano.js",
|
|
9
|
+
"hispano-lang": "./bin/hispano.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist/",
|
|
13
|
+
"bin/",
|
|
14
|
+
"README.md",
|
|
15
|
+
"LICENSE"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"start": "node main.js",
|
|
19
|
+
"test": "node test/test.js",
|
|
20
|
+
"dev": "nodemon main.js",
|
|
21
|
+
"lint": "eslint src/ test/ --ext .js",
|
|
22
|
+
"format": "prettier --write src/ test/ *.js",
|
|
23
|
+
"coverage": "echo 'Coverage not configured yet'",
|
|
24
|
+
"build": "npm run build:types && npm run build:js",
|
|
25
|
+
"build:js": "mkdir -p dist && cp -r src/* dist/ && cp main.js dist/index.js",
|
|
26
|
+
"build:types": "mkdir -p dist && node scripts/generate-types.js",
|
|
27
|
+
"demo": "node main.js",
|
|
28
|
+
"prepublishOnly": "npm run build",
|
|
29
|
+
"prepack": "npm run build"
|
|
30
|
+
},
|
|
31
|
+
"keywords": [
|
|
32
|
+
"programming-language",
|
|
33
|
+
"education",
|
|
34
|
+
"spanish",
|
|
35
|
+
"interpreter",
|
|
36
|
+
"javascript",
|
|
37
|
+
"learning",
|
|
38
|
+
"español",
|
|
39
|
+
"lenguaje-programacion",
|
|
40
|
+
"educativo",
|
|
41
|
+
"intérprete"
|
|
42
|
+
],
|
|
43
|
+
"author": {
|
|
44
|
+
"name": "Nicolas Vazquez",
|
|
45
|
+
"email": "nicorvazquezs@gmail.com",
|
|
46
|
+
"url": "https://github.com/nicvazquezdev"
|
|
47
|
+
},
|
|
48
|
+
"license": "MIT",
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20.0.0"
|
|
51
|
+
},
|
|
52
|
+
"repository": {
|
|
53
|
+
"type": "git",
|
|
54
|
+
"url": "https://github.com/nicvazquezdev/hispano-lang.git"
|
|
55
|
+
},
|
|
56
|
+
"bugs": {
|
|
57
|
+
"url": "https://github.com/nicvazquezdev/hispano-lang/issues"
|
|
58
|
+
},
|
|
59
|
+
"homepage": "https://github.com/nicvazquezdev/hispano-lang#readme",
|
|
60
|
+
"devDependencies": {
|
|
61
|
+
"nodemon": "^3.1.10",
|
|
62
|
+
"eslint": "^8.57.0",
|
|
63
|
+
"prettier": "^3.2.5"
|
|
64
|
+
},
|
|
65
|
+
"dependencies": {
|
|
66
|
+
"readline-sync": "^1.4.10"
|
|
67
|
+
},
|
|
68
|
+
"preferGlobal": true,
|
|
69
|
+
"publishConfig": {
|
|
70
|
+
"access": "public"
|
|
71
|
+
}
|
|
72
|
+
}
|