rip-lang 3.13.136 → 3.14.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.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rip-lang",
3
- "version": "3.13.136",
3
+ "version": "3.14.0",
4
4
  "description": "A modern language that compiles to JavaScript",
5
5
  "type": "module",
6
6
  "main": "src/compiler.js",
@@ -27,8 +27,7 @@
27
27
  "scripts/serve.js",
28
28
  "rip-loader.js",
29
29
  "README.md",
30
- "LICENSE",
31
- "CHANGELOG.md"
30
+ "LICENSE"
32
31
  ],
33
32
  "scripts": {
34
33
  "build": "bun scripts/build.js",
@@ -36,9 +35,16 @@
36
35
  "gen:dom": "bun scripts/gen-dom.js",
37
36
  "gallery": "bun scripts/gallery.js",
38
37
  "parser": "bun src/grammar/solar.rip -o src/parser.js src/grammar/grammar.rip",
38
+ "postinstall": "bun scripts/link-local.js --quiet && bun scripts/link-check.js --quiet",
39
+ "link-local": "bun scripts/link-local.js",
40
+ "link-global": "bun scripts/link-global.js",
41
+ "link-check": "bun scripts/link-check.js",
39
42
  "serve": "bun scripts/serve.js",
40
43
  "test": "bun test/runner.js",
41
- "test:types": "cd test/types && ../../bin/rip check && bunx tsc",
44
+ "test:types": "bun test/types/runner.js",
45
+ "test:server": "./bin/rip packages/server/tests/runner.rip",
46
+ "test:time": "bun run --cwd packages/time test",
47
+ "test:all": "bun run test && bun run test:types && bun run test:server && bun run test:time",
42
48
  "test:ui": "bun run --cwd packages/ui test:e2e",
43
49
  "test:ui:chromium": "bun run --cwd packages/ui test:e2e:chromium",
44
50
  "test:ui:axe": "bun run --cwd packages/ui test:e2e:axe",
package/src/AGENTS.md CHANGED
@@ -300,6 +300,12 @@ div.card.active
300
300
  .card.primary.("flex", x)
301
301
  ```
302
302
 
303
+ Tag and component name resolution:
304
+
305
+ - **Lowercase names** that match the template tag set are **DOM elements** (`div`, `span`, `button`)
306
+ - **PascalCase names** are **child components** — must start with uppercase, contain at least one lowercase letter, and have no underscores (`App`, `AuthScreen`, `HomeSection`)
307
+ - **ALL_CAPS names** are treated as regular variables, not components (`DEFAULT_SCREEN`, `SECTIONS`, `A`, `IO`)
308
+
303
309
  Key mechanisms:
304
310
 
305
311
  - `startsWithTag` — backward scan to decide whether a line starts a template tag
@@ -527,6 +533,130 @@ Types are processed at the token layer before parsing.
527
533
 
528
534
  `rip --shadow file.rip` dumps the virtual TypeScript file that `rip check` and the VS Code extension feed into the TypeScript language service.
529
535
 
536
+ ---
537
+
538
+ ## Schema System
539
+
540
+ Inline schemas are a third compiler sidecar — `schema.js` — that parallels
541
+ `types.js` and `components.js`.
542
+
543
+ ### Lexer path
544
+
545
+ - `installSchemaSupport(Lexer, CodeEmitter)` adds `rewriteSchema()` to the
546
+ Lexer prototype. It runs between `rewriteRender()` and `rewriteTypes()` in
547
+ the rewriter pipeline.
548
+ - `rewriteSchema()` detects a contextual `schema` identifier at expression-
549
+ start positions followed by either a `:kind` SYMBOL or a direct INDENT.
550
+ The matching INDENT...OUTDENT range is parsed by a schema-specific
551
+ sub-parser and collapsed into a single `SCHEMA_BODY` token whose `.data`
552
+ carries a structured descriptor (kind, entries, per-entry `.loc`).
553
+ - The main grammar has one tiny production, `Schema: SCHEMA SCHEMA_BODY`,
554
+ under `Expression`. Schema body syntax — field declarations (with
555
+ optional type, `min..max` range, `[default]`, `/regex/`, `{attrs}`,
556
+ terminal `-> transform` with whole-raw-input `it`), directives
557
+ (`@name`), methods (`name: -> body`), computed getters
558
+ (`name: ~> body`), eager-derived fields (`name: !> body`), and
559
+ `@ensure "msg", (x) -> predicate` refinements — never reaches the
560
+ main parser, so the state table stays lean.
561
+ - Bodies of methods, computed getters, and hooks are captured as token
562
+ slices. At codegen time those slices run through the tail rewriter
563
+ passes (implicit braces, tagged templates, etc.) and feed into
564
+ `parser.parse()` via a temporary lex adapter. The parsed body is wrapped
565
+ as a thin-arrow `['->', [], body]` AST and emitted through the existing
566
+ codegen path — Rip `->` is already a `function()` (not a JS arrow), so
567
+ `this` binds to the instance.
568
+
569
+ ### Layered runtime
570
+
571
+ The descriptor passed to `__schema({...})` is Layer 1. Layer 2 normalization
572
+ (fields, methods, computed, hooks, relations, expanded mixins, collision
573
+ checks) runs once per schema on first downstream use. Layer 3 (validator
574
+ plan) builds on the first `.parse/.safe/.ok`; Layer 4a (ORM) on the first
575
+ `.find/.create/.save`; Layer 4b (DDL) on the first `.toSQL()`. The four
576
+ caches are independent — a DDL-emitting script that only calls `.toSQL()`
577
+ never builds the ORM plan.
578
+
579
+ ### Registry
580
+
581
+ `__SchemaRegistry` (process-global) holds every named `:model` and `:mixin`.
582
+ Relations look up `:model` targets by name; `@mixin Name` looks up `:mixin`
583
+ targets. Registration happens in the `__SchemaDef` constructor so just
584
+ importing a file that defines named schemas activates them. Tests can call
585
+ `__SchemaRegistry.reset()` between runs.
586
+
587
+ ### Algebra invariant
588
+
589
+ `.pick/.omit/.partial/.required/.extend` always return `kind: 'shape'`.
590
+ **Field semantics survive** — type (including literal unions), modifiers,
591
+ constraints, and **inline transforms** — because they describe how a
592
+ field's value is obtained from raw input, not what the instance does.
593
+ **Instance behavior drops** — methods, computed getters (`~>`),
594
+ eager-derived fields (`!>`), hooks, ORM methods, and `@ensure`
595
+ refinements. Calling `.find()` or `.toSQL()` on a derived shape throws
596
+ a dedicated error pointing the user at query projection on the source
597
+ model. Refinements drop because they're schema-level invariants that
598
+ reference field names by identifier — the algebra operation has no
599
+ static way to know which names survive the derivation, so the safe
600
+ rule is "never carry them through." Runtime tests and the shadow TS
601
+ signatures both enforce this.
602
+
603
+ ### Parser invariants (don't break these)
604
+
605
+ - **Field-line classification**: `IDENTIFIER` start → field; `PROPERTY`
606
+ start (trailing `:` absorbed into the identifier's tag) → callable.
607
+ Don't merge the two paths.
608
+ - **Type slot is optional** and defaults to `string` — the parser only
609
+ consumes a type when `typeFirst[0] === 'IDENTIFIER'` or `'STRING'`
610
+ (the literal-union case). Anything else triggers the default.
611
+ - **Transform is terminal**: once `->` appears as a field-line part,
612
+ no further comma-separated parts are allowed. Reject with a diagnostic
613
+ that says "move everything else before the arrow".
614
+ - **Comma before `->` is required** whenever anything precedes it
615
+ (type, range, regex, default, attrs). Only the bare form `name! -> fn`
616
+ parses comma-less. There are two enforcement points: after type
617
+ consumption (if `rest[0]` is `->`), and inside the parts loop (via
618
+ `findTopLevelArrowIdx` scanning depth-zero arrows within a part).
619
+ - **Transforms run on `.parse()` only, never `_hydrate`.** Hydrate
620
+ bypasses the whole parse pipeline (`_applyTransforms` → defaults →
621
+ validation → assign) and goes directly from column row to instance.
622
+ - **Eager-derived (`!>`) runs on both parse AND hydrate** — in
623
+ declaration order, on the partially-constructed instance. It is
624
+ NOT re-run on field mutation (materialized once, stored as own
625
+ enumerable property).
626
+ - **`@ensure` is a special directive** — parsed into its own
627
+ `tag: 'ensure'` entry (distinct from generic `tag: 'directive'`)
628
+ because it carries a compiled fn + message, not just args. Both
629
+ inline (`@ensure "msg", (x) -> body`) and array
630
+ (`@ensure [ "msg", fn, "msg", fn ]`) forms compile to one entry
631
+ per refinement; downstream runtime can't tell them apart. The
632
+ array-form splitter treats both `,` and TERMINATOR as element
633
+ separators at depth 0 to match Rip's array-literal convention.
634
+ - **@ensure runs AFTER field validation and BEFORE eager-derived.**
635
+ `.parse/.safe/.ok` short-circuit `@ensure` when per-field errors
636
+ fire (predicates assume field types are correct). `_hydrate` skips
637
+ `@ensure` entirely (trusted data). Runtime method name
638
+ `_applyEnsures` mirrors the directive (parallel to
639
+ `_applyTransforms` for `-> transform` and `_applyEagerDerived` for
640
+ `!> derived`). See `src/schema.js`.
641
+
642
+ ### Shadow TS
643
+
644
+ `emitSchemaTypes(sexpr, lines)` walks the parsed s-expression for named
645
+ schema declarations, emits mixins first so intersections resolve, then
646
+ emits type aliases and `declare const` per kind:
647
+
648
+ - `:input` → `Schema<ValueType, ValueType>`
649
+ - `:shape` → `Schema<ShapeInstance, ShapeData>` (or `Schema<Data, Data>` when
650
+ fields-only)
651
+ - `:model` → `ModelSchema<Instance, Data>` with ORM methods and relation
652
+ accessors (same-file targets typed, cross-file degrades to `unknown`)
653
+ - `:mixin` → field-only `type Foo = { ... }` alias, no runtime value
654
+ - `:enum` → discriminated-union alias + const with `ok(data): data is
655
+ Role` type predicate
656
+
657
+ `hasSchemas(source)` is the cheap probe that gates intrinsic preamble
658
+ injection and file-level type checking (parallels `hasTypeAnnotations`).
659
+
530
660
  Typical debugging sequence:
531
661
 
532
662
  ```bash
package/src/compiler.js CHANGED
@@ -12,6 +12,7 @@ import { Lexer } from './lexer.js';
12
12
  import { parser } from './parser.js';
13
13
  import { installComponentSupport } from './components.js';
14
14
  import { emitTypes, emitEnum } from './types.js';
15
+ import { installSchemaSupport } from './schema.js';
15
16
  import { SourceMapGenerator } from './sourcemaps.js';
16
17
  import { RipError, toRipError } from './error.js';
17
18
 
@@ -251,6 +252,9 @@ export class CodeEmitter {
251
252
  // Types
252
253
  'enum': 'emitEnum',
253
254
 
255
+ // Schema
256
+ 'schema': 'emitSchema',
257
+
254
258
  // Modules
255
259
  'import': 'emitImport',
256
260
  'export': 'emitExport',
@@ -263,6 +267,9 @@ export class CodeEmitter {
263
267
  'regex': 'emitRegex',
264
268
  'tagged-template': 'emitTaggedTemplate',
265
269
  'str': 'emitString',
270
+
271
+ // Symbol literals
272
+ 'symbol': 'emitSymbol',
266
273
  };
267
274
 
268
275
  constructor(options = {}) {
@@ -318,12 +325,30 @@ export class CodeEmitter {
318
325
  }
319
326
  }
320
327
 
328
+ // Check whether a column position falls inside a string literal on a line of
329
+ // generated JavaScript/TypeScript. Used by recordSubMappings to skip false
330
+ // matches (e.g. identifiers appearing as values inside union type strings).
331
+ static _isColInsideString(line, col) {
332
+ let inStr = false, quote = '';
333
+ for (let i = 0; i < line.length && i < col; i++) {
334
+ let ch = line[i];
335
+ if (inStr) {
336
+ if (ch === '\\') { i++; continue; }
337
+ if (ch === quote) inStr = false;
338
+ } else if (ch === '"' || ch === "'" || ch === '`') {
339
+ inStr = true; quote = ch;
340
+ }
341
+ }
342
+ return inStr;
343
+ }
344
+
321
345
  // Walk the s-expression tree and record source map entries for
322
346
  // sub-expressions that carry .loc, giving column-level precision.
323
347
  recordSubMappings(code, sexpr, lineOffset) {
324
348
  let stmtOrigLine = sexpr.loc ? sexpr.loc.r : 0;
325
349
  let subs = [];
326
350
  this.collectSubExprs(sexpr, subs);
351
+ let codeLines = code.split('\n');
327
352
  for (let { name, origLine, origCol } of subs) {
328
353
  let escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
329
354
  let re = new RegExp('\\b' + escaped + '\\b', 'g');
@@ -332,11 +357,15 @@ export class CodeEmitter {
332
357
  while ((m = re.exec(code)) !== null) {
333
358
  let before = code.substring(0, m.index);
334
359
  let nl = before.split('\n');
335
- let genLine = lineOffset + nl.length - 1;
360
+ let genLineInStmt = nl.length - 1;
336
361
  let genCol = nl[nl.length - 1].length;
362
+ // Skip matches inside string literals — prevents false mappings when
363
+ // an identifier also appears as a string value (e.g. union type member)
364
+ let lineText = codeLines[genLineInStmt];
365
+ if (lineText && CodeEmitter._isColInsideString(lineText, genCol)) continue;
366
+ let genLine = lineOffset + genLineInStmt;
337
367
  // Prefer matches on the same relative line within the statement,
338
368
  // falling back to column distance as tiebreaker.
339
- let genLineInStmt = nl.length - 1;
340
369
  let dist = Math.abs(genLineInStmt - origLineInStmt) * 10000 + Math.abs(genCol - origCol);
341
370
  if (dist < bestDist) { bestDist = dist; bestMatch = { genLine, genCol }; }
342
371
  }
@@ -770,6 +799,17 @@ export class CodeEmitter {
770
799
  needsBlank = true;
771
800
  }
772
801
 
802
+ if (this.usesSchemas && !skip) {
803
+ if (skipRT) {
804
+ code += 'var { __schema, SchemaError, __SchemaRegistry, __schemaSetAdapter } = globalThis.__ripSchema;\n';
805
+ } else if (typeof globalThis !== 'undefined' && globalThis.__ripSchema) {
806
+ code += 'const { __schema, SchemaError, __SchemaRegistry, __schemaSetAdapter } = globalThis.__ripSchema;\n';
807
+ } else {
808
+ code += this.getSchemaRuntime();
809
+ }
810
+ needsBlank = true;
811
+ }
812
+
773
813
  if (this.dataSection !== null && this.dataSection !== undefined && !skip) {
774
814
  code += 'var DATA;\n_setDataSection();\n';
775
815
  needsBlank = true;
@@ -971,13 +1011,18 @@ export class CodeEmitter {
971
1011
 
972
1012
  const prevComponentName = this._componentName;
973
1013
  const prevComponentTypeParams = this._componentTypeParams;
1014
+ const prevSchemaName = this._schemaName;
974
1015
  if (this.is(value, 'component') && (typeof target === 'string' || target instanceof String)) {
975
1016
  this._componentName = str(target);
976
1017
  this._componentTypeParams = target.typeParams || '';
977
1018
  }
1019
+ if (this.is(value, 'schema') && (typeof target === 'string' || target instanceof String)) {
1020
+ this._schemaName = str(target);
1021
+ }
978
1022
  let valueCode = this.emit(value, 'value');
979
1023
  this._componentName = prevComponentName;
980
1024
  this._componentTypeParams = prevComponentTypeParams;
1025
+ this._schemaName = prevSchemaName;
981
1026
  let isObjLit = this.is(value, 'object');
982
1027
  if (!isObjLit) valueCode = this.unwrap(valueCode);
983
1028
 
@@ -1630,6 +1675,12 @@ export class CodeEmitter {
1630
1675
  return `(${ops.map(o => this.emit(o, 'value')).join(' || ')})`;
1631
1676
  }
1632
1677
 
1678
+ // ---------------------------------------------------------------------------
1679
+ // Symbol literals
1680
+ // ---------------------------------------------------------------------------
1681
+
1682
+ emitSymbol(head, rest) { return `Symbol.for(${JSON.stringify(rest[0])})`; }
1683
+
1633
1684
  // ---------------------------------------------------------------------------
1634
1685
  // Data structures
1635
1686
  // ---------------------------------------------------------------------------
@@ -2162,13 +2213,16 @@ export class CodeEmitter {
2162
2213
  if (this.is(decl, '=')) {
2163
2214
  const prev = this._componentName;
2164
2215
  const prevTP = this._componentTypeParams;
2216
+ const prevSchema = this._schemaName;
2165
2217
  if (this.is(decl[2], 'component')) {
2166
2218
  this._componentName = str(decl[1]);
2167
2219
  this._componentTypeParams = decl[1]?.typeParams || '';
2168
2220
  }
2221
+ if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
2169
2222
  const result = `const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
2170
2223
  this._componentName = prev;
2171
2224
  this._componentTypeParams = prevTP;
2225
+ this._schemaName = prevSchema;
2172
2226
  return result;
2173
2227
  }
2174
2228
  if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return '';
@@ -2177,13 +2231,16 @@ export class CodeEmitter {
2177
2231
  if (this.is(decl, '=')) {
2178
2232
  const prev = this._componentName;
2179
2233
  const prevTP = this._componentTypeParams;
2234
+ const prevSchema = this._schemaName;
2180
2235
  if (this.is(decl[2], 'component')) {
2181
2236
  this._componentName = str(decl[1]);
2182
2237
  this._componentTypeParams = decl[1]?.typeParams || '';
2183
2238
  }
2239
+ if (this.is(decl[2], 'schema')) this._schemaName = str(decl[1]);
2184
2240
  const result = `export const ${decl[1]} = ${this.emit(decl[2], 'value')}`;
2185
2241
  this._componentName = prev;
2186
2242
  this._componentTypeParams = prevTP;
2243
+ this._schemaName = prevSchema;
2187
2244
  return result;
2188
2245
  }
2189
2246
  if (Array.isArray(decl) && decl.every(i => typeof i === 'string')) return `export { ${decl.join(', ')} }`;
@@ -3555,6 +3612,12 @@ installComponentSupport(CodeEmitter, Lexer);
3555
3612
 
3556
3613
  CodeEmitter.prototype.emitEnum = emitEnum;
3557
3614
 
3615
+ // =============================================================================
3616
+ // Schema Support (prototype installation)
3617
+ // =============================================================================
3618
+
3619
+ installSchemaSupport(null, CodeEmitter);
3620
+
3558
3621
  // =============================================================================
3559
3622
  // Convenience Functions
3560
3623
  // =============================================================================
package/src/components.js CHANGED
@@ -145,7 +145,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
145
145
 
146
146
  let isComponent = (name) => {
147
147
  if (!name || typeof name !== 'string') return false;
148
- return /^[A-Z]/.test(name);
148
+ return /^[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$/.test(name);
149
149
  };
150
150
 
151
151
  let isTemplateTag = (name) => {
@@ -538,7 +538,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
538
538
  */
539
539
  proto.isComponent = function(name) {
540
540
  if (!name || typeof name !== 'string') return false;
541
- return /^[A-Z]/.test(name);
541
+ return /^[A-Z][A-Za-z0-9]*[a-z][A-Za-z0-9]*$/.test(name);
542
542
  };
543
543
 
544
544
  /**
@@ -1132,7 +1132,9 @@ export function installComponentSupport(CodeEmitter, Lexer) {
1132
1132
  return; // Don't walk children again below
1133
1133
  }
1134
1134
  } else if (head === '__text__') {
1135
- // = expr — text expression: emit the expression for type-checking
1135
+ // = expr — text expression: emit the expression for type-checking.
1136
+ // Return early — the expression is fully handled; walking children
1137
+ // would mis-interpret the call target as an element tag name.
1136
1138
  const textExpr = node[1];
1137
1139
  if (textExpr != null) {
1138
1140
  const exprCode = this.emitInComponent(textExpr, 'value');
@@ -1140,6 +1142,18 @@ export function installComponentSupport(CodeEmitter, Lexer) {
1140
1142
  const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1141
1143
  constructions.push(` ${exprCode};${srcMarker}`);
1142
1144
  }
1145
+ return;
1146
+ } else if (head === 'str') {
1147
+ // Interpolated string — emit the full expression so TS sees
1148
+ // references to variables/functions inside the interpolation
1149
+ // (e.g. "#{format(x)}" must count as a read of `format`).
1150
+ try {
1151
+ const exprCode = this.emitInComponent(node, 'value');
1152
+ const srcLine = node.loc?.r;
1153
+ const srcMarker = srcLine != null ? ` // @rip-src:${srcLine}` : '';
1154
+ constructions.push(` ${exprCode};${srcMarker}`);
1155
+ } catch {}
1156
+ return;
1143
1157
  }
1144
1158
 
1145
1159
  // Emit a bare lowercase identifier as either a property access
@@ -1534,7 +1548,7 @@ export function installComponentSupport(CodeEmitter, Lexer) {
1534
1548
  const statements = this.is(body, 'block') ? body.slice(1) : [body];
1535
1549
 
1536
1550
  let rootVar;
1537
- if (statements.length === 0) {
1551
+ if (statements.length === 0 || (statements.length === 1 && statements[0] === 'null')) {
1538
1552
  rootVar = 'null';
1539
1553
  } else if (statements.length === 1) {
1540
1554
  this._pendingAutoWire = !!this._autoEventHandlers;
@@ -2895,7 +2909,7 @@ class __Component {
2895
2909
  this._target = target;
2896
2910
  try {
2897
2911
  this._root = this._create();
2898
- target.appendChild(this._root);
2912
+ if (this._root) target.appendChild(this._root);
2899
2913
  if (this._setup) this._setup();
2900
2914
  if (this.mounted) this.mounted();
2901
2915
  } catch (error) {
@@ -86,6 +86,7 @@ grammar =
86
86
  o 'Yield'
87
87
  o 'Def'
88
88
  o 'Enum'
89
+ o 'Schema'
89
90
  ]
90
91
 
91
92
  # Single-line expressions (for postfix forms and inline arrows).
@@ -122,6 +123,7 @@ grammar =
122
123
  o 'BOOL' # true/false
123
124
  o 'INFINITY' # Infinity
124
125
  o 'NAN' # NaN
126
+ o 'SYMBOL' , '["symbol", 1]'
125
127
  ]
126
128
 
127
129
  AlphaNumeric: [
@@ -360,6 +362,7 @@ grammar =
360
362
  o 'Array'
361
363
  o 'Object'
362
364
  o 'Parenthetical'
365
+ o 'SYMBOL' , '["symbol", 1]'
363
366
  ]
364
367
 
365
368
  # ============================================================================
@@ -760,6 +763,21 @@ grammar =
760
763
  o 'ENUM Identifier Block', '["enum", 2, 3]'
761
764
  ]
762
765
 
766
+ # ============================================================================
767
+ # Schema
768
+ # ============================================================================
769
+ # `schema [:kind] INDENT ... OUTDENT` is parsed entirely by the schema
770
+ # sub-parser at lexer-rewrite time (src/schema.js). The rewriter emits
771
+ # a synthetic SCHEMA_BODY token whose .data carries the full descriptor
772
+ # (kind, entries, per-entry loc). The main grammar only sees these two
773
+ # terminals, which keeps schema body syntax (`name! type`, `@directive`,
774
+ # `name: -> body`, `name: ~> body`) fully localized and prevents
775
+ # state-table growth.
776
+
777
+ Schema: [
778
+ o 'SCHEMA SCHEMA_BODY', '["schema", 2]'
779
+ ]
780
+
763
781
  # ============================================================================
764
782
  # Components
765
783
  # ============================================================================
@@ -793,7 +811,8 @@ grammar =
793
811
  # div.card
794
812
  # h1 "Hello"
795
813
  Render: [
796
- o 'RENDER Block', '["render", 2]'
814
+ o 'RENDER Block' , '["render", 2]'
815
+ o 'RENDER Expression' , '["render", 2]'
797
816
  ]
798
817
 
799
818
  # ============================================================================
package/src/lexer.js CHANGED
@@ -40,6 +40,7 @@
40
40
  // ==========================================================================
41
41
 
42
42
  import { installTypeSupport } from './types.js';
43
+ import { installSchemaSupport } from './schema.js';
43
44
 
44
45
  // ==========================================================================
45
46
  // Token Category Sets
@@ -107,6 +108,7 @@ let INDEXABLE = new Set([
107
108
  ...CALLABLE,
108
109
  'NUMBER', 'INFINITY', 'NAN', 'STRING', 'STRING_END',
109
110
  'REGEX', 'REGEX_END', 'BOOL', 'NULL', 'UNDEFINED', '}', 'MAP_END',
111
+ 'SYMBOL',
110
112
  ]);
111
113
 
112
114
  // Tokens that can follow IMPLICIT_FUNC to start an implicit call
@@ -118,6 +120,7 @@ let IMPLICIT_CALL = new Set([
118
120
  'UNDEFINED', 'NULL', 'BOOL', 'UNARY', 'DO', 'DO_IIFE',
119
121
  'YIELD', 'AWAIT', 'UNARY_MATH', 'SUPER', 'THROW',
120
122
  '@', '->', '=>', '[', '(', '{', 'MAP_START', '--', '++',
123
+ 'SYMBOL',
121
124
  ]);
122
125
 
123
126
  // Tokens that can start an implicit call (unspaced, like +/-)
@@ -133,6 +136,7 @@ let IMPLICIT_END = new Set([
133
136
  let IMPLICIT_COMMA_BEFORE_ARROW = new Set([
134
137
  'STRING', 'STRING_END', 'REGEX', 'REGEX_END', 'NUMBER',
135
138
  'BOOL', 'NULL', 'UNDEFINED', 'INFINITY', 'NAN', ']', '}', 'MAP_END',
139
+ 'SYMBOL',
136
140
  ]);
137
141
 
138
142
  // Tokens that start/end balanced pairs
@@ -506,6 +510,9 @@ export class Lexer {
506
510
  // Don't treat colon as property when in ternary context
507
511
  if (colon && prev && prev[0] === 'TERNARY') colon = null;
508
512
 
513
+ // Don't capture spaced colon when followed by identifier start (symbol literal)
514
+ if (colon && colon.length > 1 && /[a-zA-Z_$]/.test(this.chunk[idLen + colon.length])) colon = null;
515
+
509
516
  // Property vs identifier
510
517
  if (colon || (prev && (prev[0] === '.' || prev[0] === '?.' || (!prev.spaced && prev[0] === '@')))) {
511
518
  tag = 'PROPERTY';
@@ -649,6 +656,18 @@ export class Lexer {
649
656
  }
650
657
  if (/^\s+#[a-zA-Z_]/.test(this.chunk)) return 0; // let lineToken handle indentation first
651
658
  }
659
+ // Schema field modifier: `#` adjacent (unspaced) to an identifier acts
660
+ // as the unique-marker inside schema bodies (e.g. `email!# email`).
661
+ // Return 0 so literalToken emits a standalone `#` token; rewriteSchema
662
+ // absorbs it. Outside schema bodies the `#` token is harmless because
663
+ // nothing else in the grammar accepts `IDENTIFIER #` without a space.
664
+ if (this.chunk[0] === '#') {
665
+ let prev = this.prev();
666
+ if (prev && !prev.spaced && !prev.newLine &&
667
+ (prev[0] === 'IDENTIFIER' || prev[0] === 'PROPERTY')) {
668
+ return 0;
669
+ }
670
+ }
652
671
  let match = COMMENT_RE.exec(this.chunk);
653
672
  if (!match) return 0;
654
673
  return match[0].length;
@@ -1162,6 +1181,23 @@ export class Lexer {
1162
1181
  return total;
1163
1182
  }
1164
1183
 
1184
+ // --------------------------------------------------------------------------
1185
+ // Symbol Literal: :name → Symbol.for('name')
1186
+ // --------------------------------------------------------------------------
1187
+
1188
+ symbolToken() {
1189
+ if (this.chunk[0] !== ':') return 0;
1190
+ let next = this.chunk[1];
1191
+ if (!next || next === '=' || next === ':' || /\s/.test(next)) return 0;
1192
+ if (!/[a-zA-Z_$]/.test(next)) return 0;
1193
+ let match = /^((?:(?!\s)[$\w\x7f-\uffff])+)/.exec(this.chunk.slice(1));
1194
+ if (!match) return 0;
1195
+ let name = match[1];
1196
+ let total = name.length + 1;
1197
+ this.emit('SYMBOL', name, {len: total});
1198
+ return total;
1199
+ }
1200
+
1165
1201
  // --------------------------------------------------------------------------
1166
1202
  // 9. Literal Token (operators, punctuation, everything else)
1167
1203
  // --------------------------------------------------------------------------
@@ -1171,6 +1207,10 @@ export class Lexer {
1171
1207
  let wl = this.wordLiteral();
1172
1208
  if (wl) return wl;
1173
1209
 
1210
+ // :name symbol literal: :redo → Symbol.for("redo")
1211
+ let sl = this.symbolToken();
1212
+ if (sl) return sl;
1213
+
1174
1214
  let match = OPERATOR_RE.exec(this.chunk);
1175
1215
  let val = match ? match[0] : this.chunk.charAt(0);
1176
1216
  let tag = val;
@@ -1380,6 +1420,7 @@ export class Lexer {
1380
1420
  this.closeOpenIndexes();
1381
1421
  this.normalizeLines();
1382
1422
  this.rewriteRender?.();
1423
+ this.rewriteSchema?.();
1383
1424
  this.rewriteTypes();
1384
1425
  this.tagPostfixConditionals();
1385
1426
  this.rewriteTaggedTemplates();
@@ -1951,6 +1992,7 @@ export class Lexer {
1951
1992
  // ==========================================================================
1952
1993
 
1953
1994
  installTypeSupport(Lexer);
1995
+ installSchemaSupport(Lexer);
1954
1996
 
1955
1997
  // ==========================================================================
1956
1998
  // Convenience export