eyeleng 1.0.8 → 1.0.9

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/store.js CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { tripleKey, termKey, termEquals, cloneTerm } = require('./term.js');
3
+ const { tripleKey, termKey, termEquals } = require('./term.js');
4
4
 
5
5
  class TripleStore {
6
6
  constructor(triples = []) {
@@ -109,7 +109,7 @@ function smallerValues(left, right) {
109
109
  }
110
110
 
111
111
  function normalizeTriple(triple) {
112
- return { s: cloneTerm(triple.s), p: cloneTerm(triple.p), o: cloneTerm(triple.o) };
112
+ return { s: triple.s, p: triple.p, o: triple.o };
113
113
  }
114
114
 
115
115
  function bindingKey(binding) {
package/src/term.js CHANGED
@@ -17,11 +17,13 @@ function iri(value) {
17
17
  }
18
18
 
19
19
  function variable(name) {
20
- return { type: 'var', value: String(name).replace(/^[?$]/, '') };
20
+ const value = String(name);
21
+ return { type: 'var', value: value[0] === '?' || value[0] === '$' ? value.slice(1) : value };
21
22
  }
22
23
 
23
24
  function blankNode(value) {
24
- return { type: 'blank', value: String(value).replace(/^_:/, '') };
25
+ const label = String(value);
26
+ return { type: 'blank', value: label.startsWith('_:') ? label.slice(2) : label };
25
27
  }
26
28
 
27
29
  function literal(value, datatype = null, lang = null, langDir = null) {
package/src/tokenizer.js CHANGED
@@ -20,7 +20,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
20
20
 
21
21
  function current() { return source[i]; }
22
22
  function peek(n = 1) { return source[i + n]; }
23
- function startsWith(text) { return source.slice(i, i + text.length) === text; }
23
+ function startsWith(text) { return source.startsWith(text, i); }
24
24
  function advance() {
25
25
  const ch = source[i++];
26
26
  if (ch === '\n') { line += 1; column = 1; }
@@ -28,7 +28,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
28
28
  return ch;
29
29
  }
30
30
  function token(type, value, startLine, startColumn, extra = {}) {
31
- tokens.push({ type, value, line: startLine, column: startColumn, filename, ...extra });
31
+ tokens.push({ type, value, line: startLine, column: startColumn, ...extra });
32
32
  }
33
33
  function syntax(message, startLine, startColumn) {
34
34
  throw new SyntaxErrorWithLocation(message, { line: startLine, column: startColumn, filename });
@@ -36,19 +36,21 @@ function tokenize(source, filenameOrOptions = '<input>') {
36
36
 
37
37
  function readNumericLiteral() {
38
38
  let value = '';
39
- while (i < source.length && /[0-9]/.test(current())) value += advance();
40
- if (current() === '.' && /[0-9]/.test(peek())) {
41
- value += advance();
42
- while (i < source.length && /[0-9]/.test(current())) value += advance();
39
+ const start = i;
40
+ while (i < source.length && isDigitCode(source.charCodeAt(i))) { i += 1; column += 1; }
41
+ if (source[i] === '.' && isDigitCode(source.charCodeAt(i + 1))) {
42
+ i += 1; column += 1;
43
+ while (i < source.length && isDigitCode(source.charCodeAt(i))) { i += 1; column += 1; }
43
44
  }
45
+ value = source.slice(start, i);
44
46
  if (current() === 'e' || current() === 'E') {
45
47
  const saveI = i;
46
48
  const saveLine = line;
47
49
  const saveColumn = column;
48
50
  let exponent = advance();
49
51
  if (current() === '+' || current() === '-') exponent += advance();
50
- if (/[0-9]/.test(current())) {
51
- while (i < source.length && /[0-9]/.test(current())) exponent += advance();
52
+ if (isDigitCode(source.charCodeAt(i))) {
53
+ while (i < source.length && isDigitCode(source.charCodeAt(i))) exponent += advance();
52
54
  value += exponent;
53
55
  } else {
54
56
  i = saveI;
@@ -66,7 +68,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
66
68
  const length = esc === 'u' ? 4 : 8;
67
69
  let hex = '';
68
70
  for (let j = 0; j < length; j += 1) {
69
- if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
71
+ if (!isHexCode(source.charCodeAt(i))) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
70
72
  hex += advance();
71
73
  }
72
74
  const codePoint = Number.parseInt(hex, 16);
@@ -85,7 +87,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
85
87
  const length = esc === 'u' ? 4 : 8;
86
88
  let hex = '';
87
89
  for (let j = 0; j < length; j += 1) {
88
- if (!/[0-9A-Fa-f]/.test(current() || '')) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
90
+ if (!isHexCode(source.charCodeAt(i))) syntax(`Invalid \\${esc} escape`, startLine, startColumn);
89
91
  hex += advance();
90
92
  }
91
93
  const codePoint = Number.parseInt(hex, 16);
@@ -99,7 +101,7 @@ function tokenize(source, filenameOrOptions = '<input>') {
99
101
 
100
102
  while (i < source.length) {
101
103
  const ch = current();
102
- if (/\s/.test(ch)) { advance(); continue; }
104
+ if (isWhitespaceCode(source.charCodeAt(i))) { advance(); continue; }
103
105
  if (ch === '#') {
104
106
  while (i < source.length && current() !== '\n') advance();
105
107
  continue;
@@ -185,18 +187,21 @@ function tokenize(source, filenameOrOptions = '<input>') {
185
187
  }
186
188
 
187
189
  if (ch === '@') {
188
- let value = advance();
189
- while (i < source.length && /[A-Za-z0-9-]/.test(current())) value += advance();
190
+ const wordStart = i;
191
+ advance();
192
+ while (i < source.length && isLangTagCode(source.charCodeAt(i))) { i += 1; column += 1; }
193
+ const value = source.slice(wordStart, i);
190
194
  if (!/^@[A-Za-z]+(?:-[A-Za-z0-9]+)*(?:--[A-Za-z]+)?$/.test(value)) syntax(`Invalid language tag ${value}`, startLine, startColumn);
191
195
  token('word', value, startLine, startColumn);
192
196
  continue;
193
197
  }
194
198
 
195
199
  if (ch === '?' || ch === '$') {
196
- let value = advance();
197
- while (i < source.length && /[A-Za-z0-9_\-]/.test(current())) value += advance();
198
- if (value.length === 1) syntax('Expected variable name', startLine, startColumn);
199
- token('variable', value.slice(1), startLine, startColumn);
200
+ const varStart = i;
201
+ advance();
202
+ while (i < source.length && isVarNameCode(source.charCodeAt(i))) { i += 1; column += 1; }
203
+ if (i - varStart === 1) syntax('Expected variable name', startLine, startColumn);
204
+ token('variable', source.slice(varStart + 1, i), startLine, startColumn);
200
205
  continue;
201
206
  }
202
207
 
@@ -223,24 +228,27 @@ function tokenize(source, filenameOrOptions = '<input>') {
223
228
  continue;
224
229
  }
225
230
 
226
- let value = '';
231
+ const wordStart = i;
227
232
  while (i < source.length) {
228
- const c = current();
229
- if (c === '\\' && peek() !== undefined) {
230
- value += advance();
231
- value += advance();
233
+ const c = source[i];
234
+ if (c === '\\' && source[i + 1] !== undefined) {
235
+ i += 2;
236
+ column += 2;
232
237
  continue;
233
238
  }
234
- if (/\s/.test(c) || '{}()[],;|'.includes(c) || '=<>+-*/!^~'.includes(c)) break;
239
+ const code = source.charCodeAt(i);
240
+ if (isWhitespaceCode(code) || '{}()[],;|'.includes(c) || '=<>+-*/!^~'.includes(c)) break;
235
241
  if (c === '.') {
236
- const n = peek();
237
- if (n === undefined || /\s/.test(n) || '{}()[],;|'.includes(n) || '=<>+-*/!^~'.includes(n)) break;
242
+ const n = source[i + 1];
243
+ if (n === undefined || isWhitespaceCode(n.charCodeAt(0)) || '{}()[],;|'.includes(n) || '=<>+-*/!^~'.includes(n)) break;
238
244
  }
239
245
  if (c === '#') break;
240
- value += advance();
246
+ i += 1;
247
+ column += 1;
241
248
  }
242
- if (value.length === 0) syntax(`Unexpected character ${JSON.stringify(ch)}`, startLine, startColumn);
249
+ if (i === wordStart) syntax(`Unexpected character ${JSON.stringify(ch)}`, startLine, startColumn);
243
250
 
251
+ const value = source.slice(wordStart, i);
244
252
  if (/^[+-]?(?:(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?|\d+[eE][+-]?\d+|\d+)$/.test(value)) token('number', Number(value), startLine, startColumn);
245
253
  else token('word', value, startLine, startColumn);
246
254
  }
@@ -249,11 +257,32 @@ function tokenize(source, filenameOrOptions = '<input>') {
249
257
  return tokens;
250
258
  }
251
259
 
260
+
261
+ function isDigitCode(code) {
262
+ return code >= 48 && code <= 57;
263
+ }
264
+
265
+ function isHexCode(code) {
266
+ return (code >= 48 && code <= 57) || (code >= 65 && code <= 70) || (code >= 97 && code <= 102);
267
+ }
268
+
269
+ function isWhitespaceCode(code) {
270
+ return code === 32 || code === 9 || code === 10 || code === 13 || code === 12;
271
+ }
272
+
273
+ function isLangTagCode(code) {
274
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 45;
275
+ }
276
+
277
+ function isVarNameCode(code) {
278
+ return (code >= 65 && code <= 90) || (code >= 97 && code <= 122) || (code >= 48 && code <= 57) || code === 95 || code === 45;
279
+ }
280
+
252
281
  function startsNumericLiteral(source, i) {
253
282
  const ch = source[i];
254
283
  const next = source[i + 1];
255
- if (/[0-9]/.test(ch)) return true;
256
- if (ch === '.' && /[0-9]/.test(next)) return true;
284
+ if (isDigitCode(ch.charCodeAt(0))) return true;
285
+ if (ch === '.' && next !== undefined && isDigitCode(next.charCodeAt(0))) return true;
257
286
  return false;
258
287
  }
259
288
 
@@ -0,0 +1,39 @@
1
+ {
2
+ "version": 1,
3
+ "generatedBy": "npm run bench:update",
4
+ "note": "Machine-dependent performance baseline. Use npm run bench:update to refresh intentionally.",
5
+ "tolerance": {
6
+ "relative": 1.35,
7
+ "absoluteMs": 1000
8
+ },
9
+ "cases": [
10
+ {
11
+ "name": "deep-taxonomy-100000.srl",
12
+ "file": "examples/deep-taxonomy-100000.srl",
13
+ "repeat": 1,
14
+ "baselineMs": 5427.8,
15
+ "outputBytes": 5068161
16
+ },
17
+ {
18
+ "name": "fibonacci.srl",
19
+ "file": "examples/fibonacci.srl",
20
+ "repeat": 1,
21
+ "baselineMs": 1409.4,
22
+ "outputBytes": 10810713
23
+ },
24
+ {
25
+ "name": "fft32-numeric.srl",
26
+ "file": "examples/fft32-numeric.srl",
27
+ "repeat": 3,
28
+ "baselineMs": 182.2,
29
+ "outputBytes": 466351
30
+ },
31
+ {
32
+ "name": "path-discovery.srl",
33
+ "file": "examples/path-discovery.srl",
34
+ "repeat": 5,
35
+ "baselineMs": 1317.7,
36
+ "outputBytes": 736870
37
+ }
38
+ ]
39
+ }
@@ -0,0 +1,234 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+ const path = require('node:path');
6
+ const { performance } = require('node:perf_hooks');
7
+ const { pathToFileURL, fileURLToPath } = require('node:url');
8
+ const { runToString } = require('../src/index.js');
9
+
10
+ const root = path.join(__dirname, '..');
11
+ const baselinePath = path.join(root, 'test', 'perf-baseline.json');
12
+ const DEFAULT_RELATIVE_TOLERANCE = 1.35;
13
+ const DEFAULT_ABSOLUTE_TOLERANCE_MS = 1000;
14
+
15
+ const defaultCases = [
16
+ { name: 'deep-taxonomy-100000.srl', file: 'examples/deep-taxonomy-100000.srl', repeat: 1 },
17
+ { name: 'fibonacci.srl', file: 'examples/fibonacci.srl', repeat: 1 },
18
+ { name: 'fft32-numeric.srl', file: 'examples/fft32-numeric.srl', repeat: 3 },
19
+ { name: 'path-discovery.srl', file: 'examples/path-discovery.srl', repeat: 5 },
20
+ ];
21
+
22
+ function parseArgs(argv) {
23
+ const options = {
24
+ mode: 'check',
25
+ json: false,
26
+ cases: [],
27
+ repeat: null,
28
+ baseline: baselinePath,
29
+ };
30
+
31
+ for (let i = 0; i < argv.length; i += 1) {
32
+ const arg = argv[i];
33
+ if (arg === '--check') options.mode = 'check';
34
+ else if (arg === '--update') options.mode = 'update';
35
+ else if (arg === '--report') options.mode = 'report';
36
+ else if (arg === '--json') options.json = true;
37
+ else if (arg === '--case') {
38
+ i += 1;
39
+ if (i >= argv.length) throw new Error('--case requires a benchmark name or file');
40
+ options.cases.push(argv[i]);
41
+ } else if (arg === '--repeat') {
42
+ i += 1;
43
+ if (i >= argv.length) throw new Error('--repeat requires a positive integer');
44
+ options.repeat = Number(argv[i]);
45
+ if (!Number.isInteger(options.repeat) || options.repeat < 1) throw new Error('--repeat requires a positive integer');
46
+ } else if (arg === '--baseline') {
47
+ i += 1;
48
+ if (i >= argv.length) throw new Error('--baseline requires a path');
49
+ options.baseline = path.resolve(argv[i]);
50
+ } else if (arg === '-h' || arg === '--help') {
51
+ options.help = true;
52
+ } else {
53
+ throw new Error(`Unknown option ${arg}`);
54
+ }
55
+ }
56
+
57
+ return options;
58
+ }
59
+
60
+ function help() {
61
+ return `Usage: node tools/perf-bench.js [--check|--update|--report] [options]\n\nRuns selected large examples and checks them against test/perf-baseline.json.\nThis is intentionally separate from npm test so normal correctness tests do not\nfail because of machine-dependent timings.\n\nOptions:\n --check Fail if a benchmark exceeds its budget (default)\n --update Rewrite the baseline with the current timings\n --report Print timings without checking or updating\n --json Print JSON output\n --case NAME Run only a case by name or file; may be repeated\n --repeat N Override per-case repeat count\n --baseline FILE Use a different baseline file\n -h, --help Print this help\n\nEnvironment overrides:\n EYELENG_BENCH_RELATIVE_TOLERANCE default ${DEFAULT_RELATIVE_TOLERANCE}\n EYELENG_BENCH_ABSOLUTE_TOLERANCE_MS default ${DEFAULT_ABSOLUTE_TOLERANCE_MS}\n EYELENG_BENCH_REPEAT override repeat count\n`;
62
+ }
63
+
64
+ function loadBaseline(filename) {
65
+ if (!fs.existsSync(filename)) return null;
66
+ return JSON.parse(fs.readFileSync(filename, 'utf8'));
67
+ }
68
+
69
+ function baselineCases(baseline) {
70
+ if (!baseline || !Array.isArray(baseline.cases)) return null;
71
+ return baseline.cases.map((item) => ({
72
+ name: item.name,
73
+ file: item.file,
74
+ repeat: item.repeat || 1,
75
+ baselineMs: item.baselineMs,
76
+ }));
77
+ }
78
+
79
+ function selectCases(allCases, requested) {
80
+ if (requested.length === 0) return allCases;
81
+ return requested.map((name) => {
82
+ const found = allCases.find((item) => item.name === name || item.file === name || path.basename(item.file) === name);
83
+ if (!found) throw new Error(`Unknown benchmark case ${name}`);
84
+ return found;
85
+ });
86
+ }
87
+
88
+ function importResolver(target) {
89
+ if (!target.startsWith('file:')) throw new Error(`benchmark import resolver only supports file: imports, got ${target}`);
90
+ const filename = fileURLToPath(target);
91
+ return {
92
+ source: fs.readFileSync(filename, 'utf8'),
93
+ options: { filename, baseIRI: pathToFileURL(filename).href },
94
+ };
95
+ }
96
+
97
+ function runOptions(filename) {
98
+ return {
99
+ filename,
100
+ baseIRI: pathToFileURL(filename).href,
101
+ importResolver,
102
+ syntax: filename.endsWith('.ttl') ? 'rdf' : undefined,
103
+ now: new Date('2026-05-15T12:34:56Z'),
104
+ };
105
+ }
106
+
107
+ function median(values) {
108
+ const sorted = values.slice().sort((a, b) => a - b);
109
+ const middle = Math.floor(sorted.length / 2);
110
+ return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle];
111
+ }
112
+
113
+ function roundMs(ms) {
114
+ return Math.round(ms * 10) / 10;
115
+ }
116
+
117
+ function runCase(testCase, repeatOverride) {
118
+ const filename = path.join(root, testCase.file);
119
+ const source = fs.readFileSync(filename, 'utf8');
120
+ const repeat = repeatOverride || Number(process.env.EYELENG_BENCH_REPEAT) || testCase.repeat || 1;
121
+ const samples = [];
122
+ let outputBytes = 0;
123
+
124
+ for (let i = 0; i < repeat; i += 1) {
125
+ if (global.gc) global.gc();
126
+ const start = performance.now();
127
+ const output = runToString(source, runOptions(filename));
128
+ const elapsed = performance.now() - start;
129
+ outputBytes = Buffer.byteLength(output, 'utf8');
130
+ samples.push(elapsed);
131
+ }
132
+
133
+ return {
134
+ name: testCase.name,
135
+ file: testCase.file,
136
+ repeat,
137
+ samplesMs: samples.map(roundMs),
138
+ ms: roundMs(median(samples)),
139
+ outputBytes,
140
+ };
141
+ }
142
+
143
+ function toleranceFromEnv(baseline = {}) {
144
+ const fromBaseline = baseline.tolerance || {};
145
+ const relative = Number(process.env.EYELENG_BENCH_RELATIVE_TOLERANCE || fromBaseline.relative || DEFAULT_RELATIVE_TOLERANCE);
146
+ const absoluteMs = Number(process.env.EYELENG_BENCH_ABSOLUTE_TOLERANCE_MS || fromBaseline.absoluteMs || DEFAULT_ABSOLUTE_TOLERANCE_MS);
147
+ if (!Number.isFinite(relative) || relative < 1) throw new Error('relative tolerance must be >= 1');
148
+ if (!Number.isFinite(absoluteMs) || absoluteMs < 0) throw new Error('absolute tolerance must be >= 0');
149
+ return { relative, absoluteMs };
150
+ }
151
+
152
+ function budgetFor(item, tolerance) {
153
+ if (!Number.isFinite(item.baselineMs)) return null;
154
+ return roundMs(item.baselineMs * tolerance.relative + tolerance.absoluteMs);
155
+ }
156
+
157
+ function printTable(results, checks) {
158
+ const widths = {
159
+ name: Math.max('case'.length, ...results.map((item) => item.name.length)),
160
+ ms: Math.max('ms'.length, ...results.map((item) => String(item.ms).length)),
161
+ budget: Math.max('budget'.length, ...checks.map((item) => item.budgetMs == null ? 1 : String(item.budgetMs).length)),
162
+ };
163
+ console.log(`${'case'.padEnd(widths.name)} ${'ms'.padStart(widths.ms)} ${'budget'.padStart(widths.budget)} status`);
164
+ console.log(`${'-'.repeat(widths.name)} ${'-'.repeat(widths.ms)} ${'-'.repeat(widths.budget)} ------`);
165
+ for (const item of results) {
166
+ const check = checks.find((entry) => entry.name === item.name) || {};
167
+ const budget = check.budgetMs == null ? '-' : String(check.budgetMs);
168
+ const status = check.status || 'report';
169
+ console.log(`${item.name.padEnd(widths.name)} ${String(item.ms).padStart(widths.ms)} ${budget.padStart(widths.budget)} ${status}`);
170
+ }
171
+ }
172
+
173
+ function main() {
174
+ const options = parseArgs(process.argv.slice(2));
175
+ if (options.help) {
176
+ console.log(help());
177
+ return;
178
+ }
179
+
180
+ const baseline = loadBaseline(options.baseline);
181
+ const allCases = baselineCases(baseline) || defaultCases;
182
+ const selected = selectCases(allCases, options.cases);
183
+ const repeatOverride = options.repeat || null;
184
+ const results = selected.map((testCase) => runCase(testCase, repeatOverride));
185
+ const tolerance = toleranceFromEnv(baseline || {});
186
+
187
+ const byBaseline = new Map((baselineCases(baseline) || []).map((item) => [item.name, item]));
188
+ const checks = results.map((result) => {
189
+ const baselineCase = byBaseline.get(result.name);
190
+ const budgetMs = baselineCase ? budgetFor(baselineCase, tolerance) : null;
191
+ const status = budgetMs == null ? 'no-baseline' : result.ms <= budgetMs ? 'ok' : 'regressed';
192
+ return { name: result.name, baselineMs: baselineCase ? baselineCase.baselineMs : null, budgetMs, status };
193
+ });
194
+
195
+ if (options.mode === 'update') {
196
+ const updated = {
197
+ version: 1,
198
+ generatedBy: 'npm run bench:update',
199
+ note: 'Machine-dependent performance baseline. Use npm run bench:update to refresh intentionally.',
200
+ tolerance,
201
+ cases: results.map((result) => ({
202
+ name: result.name,
203
+ file: result.file,
204
+ repeat: result.repeat,
205
+ baselineMs: result.ms,
206
+ outputBytes: result.outputBytes,
207
+ })),
208
+ };
209
+ fs.mkdirSync(path.dirname(options.baseline), { recursive: true });
210
+ fs.writeFileSync(options.baseline, `${JSON.stringify(updated, null, 2)}\n`, 'utf8');
211
+ }
212
+
213
+ if (options.json) {
214
+ console.log(JSON.stringify({ mode: options.mode, tolerance, results, checks }, null, 2));
215
+ } else {
216
+ printTable(results, checks);
217
+ if (options.mode === 'update') console.log(`\nUpdated ${path.relative(root, options.baseline)}`);
218
+ }
219
+
220
+ if (options.mode === 'check') {
221
+ const regressions = checks.filter((item) => item.status === 'regressed');
222
+ if (regressions.length > 0) {
223
+ console.error('\nPerformance regression detected. Run npm run bench:update only after intentionally accepting a new baseline.');
224
+ process.exitCode = 1;
225
+ }
226
+ }
227
+ }
228
+
229
+ try {
230
+ main();
231
+ } catch (err) {
232
+ console.error(err.stack || err.message || String(err));
233
+ process.exit(1);
234
+ }