@holoscript/robotics-plugin 1.0.0 → 2.0.2

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/src/lexer.ts ADDED
@@ -0,0 +1,307 @@
1
+ /**
2
+ * HoloScript Lexer (Tokenizer)
3
+ *
4
+ * Converts HoloScript source code into a stream of tokens for parsing.
5
+ * Supports: keywords, identifiers, traits (@trait), strings, numbers, symbols
6
+ */
7
+
8
+ export enum TokenType {
9
+ // Keywords
10
+ KEYWORD = 'KEYWORD', // composition, object, using, template, group
11
+
12
+ // Identifiers & Traits
13
+ IDENTIFIER = 'IDENTIFIER', // variable_name, link1, base
14
+ TRAIT = 'TRAIT', // @joint_revolute, @force_sensor
15
+ STRING = 'STRING', // "Base", "metal", "cylinder"
16
+ NUMBER = 'NUMBER', // 0.1, 3.14, 100, -2.0
17
+
18
+ // Symbols
19
+ LBRACE = 'LBRACE', // {
20
+ RBRACE = 'RBRACE', // }
21
+ LBRACKET = 'LBRACKET', // [
22
+ RBRACKET = 'RBRACKET', // ]
23
+ COLON = 'COLON', // :
24
+ COMMA = 'COMMA', // ,
25
+
26
+ // Special
27
+ COMMENT = 'COMMENT', // // comment
28
+ EOF = 'EOF', // end of file
29
+ }
30
+
31
+ export interface Token {
32
+ type: TokenType;
33
+ value: string;
34
+ line: number;
35
+ column: number;
36
+ }
37
+
38
+ export class Lexer {
39
+ private source: string;
40
+ private position: number = 0;
41
+ private line: number = 1;
42
+ private column: number = 1;
43
+ private tokens: Token[] = [];
44
+
45
+ private readonly keywords = new Set([
46
+ 'composition',
47
+ 'object',
48
+ 'using',
49
+ 'template',
50
+ 'group',
51
+ 'scene',
52
+ ]);
53
+
54
+ constructor(source: string) {
55
+ this.source = source;
56
+ }
57
+
58
+ tokenize(): Token[] {
59
+ while (this.position < this.source.length) {
60
+ this.scanToken();
61
+ }
62
+
63
+ this.tokens.push({
64
+ type: TokenType.EOF,
65
+ value: '',
66
+ line: this.line,
67
+ column: this.column,
68
+ });
69
+
70
+ return this.tokens;
71
+ }
72
+
73
+ private scanToken(): void {
74
+ const char = this.currentChar();
75
+
76
+ // Skip whitespace
77
+ if (this.isWhitespace(char)) {
78
+ this.skipWhitespace();
79
+ return;
80
+ }
81
+
82
+ // Comments
83
+ if (char === '/' && this.peek() === '/') {
84
+ this.skipComment();
85
+ return;
86
+ }
87
+
88
+ // Traits (@identifier)
89
+ if (char === '@') {
90
+ this.scanTrait();
91
+ return;
92
+ }
93
+
94
+ // String literals ("...")
95
+ if (char === '"') {
96
+ this.scanString();
97
+ return;
98
+ }
99
+
100
+ // Numbers (0.1, 3.14, -2.0)
101
+ if (this.isDigit(char) || (char === '-' && this.isDigit(this.peek()))) {
102
+ this.scanNumber();
103
+ return;
104
+ }
105
+
106
+ // Keywords & Identifiers
107
+ if (this.isAlpha(char)) {
108
+ this.scanIdentifierOrKeyword();
109
+ return;
110
+ }
111
+
112
+ // Single-character tokens
113
+ this.scanSymbol(char);
114
+ }
115
+
116
+ private scanTrait(): void {
117
+ const startLine = this.line;
118
+ const startColumn = this.column;
119
+
120
+ this.advance(); // Skip '@'
121
+
122
+ let trait = '';
123
+ while (
124
+ this.position < this.source.length &&
125
+ this.isAlphaNumericOrUnderscore(this.currentChar())
126
+ ) {
127
+ trait += this.currentChar();
128
+ this.advance();
129
+ }
130
+
131
+ this.tokens.push({
132
+ type: TokenType.TRAIT,
133
+ value: trait,
134
+ line: startLine,
135
+ column: startColumn,
136
+ });
137
+ }
138
+
139
+ private scanString(): void {
140
+ const startLine = this.line;
141
+ const startColumn = this.column;
142
+
143
+ this.advance(); // Skip opening "
144
+
145
+ let str = '';
146
+ while (this.position < this.source.length && this.currentChar() !== '"') {
147
+ if (this.currentChar() === '\\') {
148
+ // Handle escape sequences
149
+ this.advance();
150
+ if (this.position < this.source.length) {
151
+ str += this.currentChar();
152
+ this.advance();
153
+ }
154
+ } else {
155
+ str += this.currentChar();
156
+ this.advance();
157
+ }
158
+ }
159
+
160
+ if (this.currentChar() === '"') {
161
+ this.advance(); // Skip closing "
162
+ } else {
163
+ throw new Error(`Unterminated string at line ${startLine}, column ${startColumn}`);
164
+ }
165
+
166
+ this.tokens.push({
167
+ type: TokenType.STRING,
168
+ value: str,
169
+ line: startLine,
170
+ column: startColumn,
171
+ });
172
+ }
173
+
174
+ private scanNumber(): void {
175
+ const startLine = this.line;
176
+ const startColumn = this.column;
177
+
178
+ let num = '';
179
+
180
+ // Handle negative sign
181
+ if (this.currentChar() === '-') {
182
+ num += this.currentChar();
183
+ this.advance();
184
+ }
185
+
186
+ // Integer part
187
+ while (this.position < this.source.length && this.isDigit(this.currentChar())) {
188
+ num += this.currentChar();
189
+ this.advance();
190
+ }
191
+
192
+ // Decimal part
193
+ if (this.currentChar() === '.' && this.isDigit(this.peek())) {
194
+ num += this.currentChar();
195
+ this.advance();
196
+
197
+ while (this.position < this.source.length && this.isDigit(this.currentChar())) {
198
+ num += this.currentChar();
199
+ this.advance();
200
+ }
201
+ }
202
+
203
+ this.tokens.push({
204
+ type: TokenType.NUMBER,
205
+ value: num,
206
+ line: startLine,
207
+ column: startColumn,
208
+ });
209
+ }
210
+
211
+ private scanIdentifierOrKeyword(): void {
212
+ const startLine = this.line;
213
+ const startColumn = this.column;
214
+
215
+ let ident = '';
216
+ while (
217
+ this.position < this.source.length &&
218
+ this.isAlphaNumericOrUnderscore(this.currentChar())
219
+ ) {
220
+ ident += this.currentChar();
221
+ this.advance();
222
+ }
223
+
224
+ const type = this.keywords.has(ident) ? TokenType.KEYWORD : TokenType.IDENTIFIER;
225
+
226
+ this.tokens.push({
227
+ type,
228
+ value: ident,
229
+ line: startLine,
230
+ column: startColumn,
231
+ });
232
+ }
233
+
234
+ private scanSymbol(char: string): void {
235
+ const symbolMap: Record<string, TokenType> = {
236
+ '{': TokenType.LBRACE,
237
+ '}': TokenType.RBRACE,
238
+ '[': TokenType.LBRACKET,
239
+ ']': TokenType.RBRACKET,
240
+ ':': TokenType.COLON,
241
+ ',': TokenType.COMMA,
242
+ };
243
+
244
+ const tokenType = symbolMap[char];
245
+ if (tokenType) {
246
+ this.tokens.push({
247
+ type: tokenType,
248
+ value: char,
249
+ line: this.line,
250
+ column: this.column,
251
+ });
252
+ this.advance();
253
+ } else {
254
+ throw new Error(`Unexpected character '${char}' at line ${this.line}, column ${this.column}`);
255
+ }
256
+ }
257
+
258
+ private skipWhitespace(): void {
259
+ while (this.position < this.source.length && this.isWhitespace(this.currentChar())) {
260
+ if (this.currentChar() === '\n') {
261
+ this.line++;
262
+ this.column = 1;
263
+ } else {
264
+ this.column++;
265
+ }
266
+ this.position++;
267
+ }
268
+ }
269
+
270
+ private skipComment(): void {
271
+ // Skip until end of line
272
+ while (this.position < this.source.length && this.currentChar() !== '\n') {
273
+ this.position++;
274
+ this.column++;
275
+ }
276
+ }
277
+
278
+ private currentChar(): string {
279
+ return this.source[this.position] || '';
280
+ }
281
+
282
+ private peek(offset: number = 1): string {
283
+ const peekPos = this.position + offset;
284
+ return this.source[peekPos] || '';
285
+ }
286
+
287
+ private advance(): void {
288
+ this.position++;
289
+ this.column++;
290
+ }
291
+
292
+ private isWhitespace(char: string): boolean {
293
+ return /\s/.test(char);
294
+ }
295
+
296
+ private isDigit(char: string): boolean {
297
+ return /[0-9]/.test(char);
298
+ }
299
+
300
+ private isAlpha(char: string): boolean {
301
+ return /[a-z_]/i.test(char);
302
+ }
303
+
304
+ private isAlphaNumericOrUnderscore(char: string): boolean {
305
+ return /[a-z0-9_]/i.test(char);
306
+ }
307
+ }