feat(compiler): self-hosting spec + generator multi-enum support
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
Some checks failed
Deploy Landing Page to Storj / deploy (push) Failing after 2s
- Add speckl-compile.speckdl — compiler formally specified in SpeckDL - Generator: multi-enum detection, value→enum mapping, trailing comma cleanup - Emit blocks collapsed to single-line (parser limitation) - Generated artifacts: provenance, BOMs, WAT, TypeScript (2 known bugs → engineering#8)
This commit is contained in:
parent
39436b4382
commit
7230650628
12 changed files with 814 additions and 46 deletions
34
compiler/node_modules/.package-lock.json
generated
vendored
34
compiler/node_modules/.package-lock.json
generated
vendored
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "speckl-compile",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.1",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
|
|
@ -42,6 +42,20 @@
|
|||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.60.2",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz",
|
||||
"integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@types/estree": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
|
||||
|
|
@ -1138,6 +1152,24 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/wabt": {
|
||||
"version": "1.0.39",
|
||||
"resolved": "https://registry.npmjs.org/wabt/-/wabt-1.0.39.tgz",
|
||||
"integrity": "sha512-ba+dRL/75VQQY7RkU/CgriGbkoWAfS8TDyUlJfJhJ8KhtXgMl5dhNvoPNUcQ9IWRhW8u41glMSuZeTvsYq2rRg==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"wasm-decompile": "bin/wasm-decompile",
|
||||
"wasm-interp": "bin/wasm-interp",
|
||||
"wasm-objdump": "bin/wasm-objdump",
|
||||
"wasm-stats": "bin/wasm-stats",
|
||||
"wasm-strip": "bin/wasm-strip",
|
||||
"wasm-validate": "bin/wasm-validate",
|
||||
"wasm2c": "bin/wasm2c",
|
||||
"wasm2wat": "bin/wasm2wat",
|
||||
"wat2wasm": "bin/wat2wasm"
|
||||
}
|
||||
},
|
||||
"node_modules/why-is-node-running": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
|
||||
|
|
|
|||
2
compiler/node_modules/.vite/vitest/results.json
generated
vendored
2
compiler/node_modules/.vite/vitest/results.json
generated
vendored
|
|
@ -1 +1 @@
|
|||
{"version":"2.1.9","results":[[":__tests__/parser.test.ts",{"duration":38.49205900000004,"failed":false}],[":src/__tests__/compiler.test.ts",{"duration":10.381641000000002,"failed":false}]]}
|
||||
{"version":"2.1.9","results":[[":__tests__/parser.test.ts",{"duration":41.041000999999994,"failed":false}],[":__tests__/e2e-wasm.test.ts",{"duration":108.24758300000002,"failed":false}],[":src/__tests__/compiler.test.ts",{"duration":10.95270800000003,"failed":false}]]}
|
||||
29
compiler/out/speckl-compile/SpecklCompiler.prov.jsonld
Normal file
29
compiler/out/speckl-compile/SpecklCompiler.prov.jsonld
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
{
|
||||
"@context": {
|
||||
"prov": "http://www.w3.org/ns/prov#",
|
||||
"speckl": "https://speckl.scoble.me/ns/v0.2#",
|
||||
"rdfs": "http://www.w3.org/2000/01/rdf-schema#"
|
||||
},
|
||||
"@graph": [
|
||||
{
|
||||
"@type": "prov:Entity",
|
||||
"@id": "speck:SpecklCompiler",
|
||||
"prov:qualifiedGeneration": {
|
||||
"@type": "prov:Generation",
|
||||
"prov:activity": {
|
||||
"@type": "prov:Activity",
|
||||
"prov:startTime": "2026-05-03T21:19:53.222Z"
|
||||
}
|
||||
},
|
||||
"rdfs:label": "SpecklCompiler",
|
||||
"prov:wasAttributedTo": {
|
||||
"@id": "agent:sscoble"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "prov:Agent",
|
||||
"@id": "agent:sscoble",
|
||||
"rdfs:label": "sscoble"
|
||||
}
|
||||
]
|
||||
}
|
||||
30
compiler/out/speckl-compile/SpecklCompiler.specbom.cdx.json
Normal file
30
compiler/out/speckl-compile/SpecklCompiler.specbom.cdx.json
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"tools": [],
|
||||
"component": {
|
||||
"type": "application",
|
||||
"name": "SpecklCompiler",
|
||||
"version": "0.2.0",
|
||||
"properties": []
|
||||
}
|
||||
},
|
||||
"annotations": [
|
||||
{
|
||||
"subject": "speck:SpecklCompiler",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "tag",
|
||||
"value": "provenanceType",
|
||||
"text": "author"
|
||||
}
|
||||
],
|
||||
"author": {
|
||||
"name": "sscoble"
|
||||
},
|
||||
"timestamp": "2026-05-03T21:19:53.229Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
42
compiler/out/speckl-compile/SpecklCompiler.specbom.spdx.json
Normal file
42
compiler/out/speckl-compile/SpecklCompiler.specbom.spdx.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://spdx.github.io/spdx-v3/spdx-context.jsonld",
|
||||
{
|
||||
"speckl": "https://speckl.scoble.me/ns/v0.2#"
|
||||
}
|
||||
],
|
||||
"@type": "spdx:Document",
|
||||
"spdx": {
|
||||
"@id": "spdx:SpecklCompiler",
|
||||
"@type": "spdx:Snippet",
|
||||
"name": "SpecklCompiler",
|
||||
"spdxVersion": "3.0.1",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"creationInfo": {
|
||||
"created": "2026-05-03T21:19:53.229Z",
|
||||
"createdBy": [
|
||||
"Organization: Speckl",
|
||||
"tool: speckl-compile-0.2.0"
|
||||
]
|
||||
}
|
||||
},
|
||||
"builtBy": [],
|
||||
"relationships": [
|
||||
{
|
||||
"from": {
|
||||
"@id": "spdx:SpecklCompiler"
|
||||
},
|
||||
"to": {
|
||||
"@id": "spdx:agent:sscoble"
|
||||
},
|
||||
"relationshipType": "spdx:wasAttributedTo"
|
||||
}
|
||||
],
|
||||
"elements": [
|
||||
{
|
||||
"@type": "spdx:Person",
|
||||
"@id": "spdx:agent:sscoble",
|
||||
"name": "sscoble"
|
||||
}
|
||||
]
|
||||
}
|
||||
153
compiler/out/speckl-compile/SpecklCompiler.ts
Normal file
153
compiler/out/speckl-compile/SpecklCompiler.ts
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
// Auto-generated by speckl-compile from SpecklCompiler
|
||||
// DO NOT EDIT MANUALLY — regenerate from .speckdl source
|
||||
|
||||
export const SpecklCompilerCompilerPhase = {
|
||||
BOMsGenerated: 'bomsgenerated',
|
||||
Idle: 'idle',
|
||||
Parsed: 'parsed',
|
||||
Parsing: 'parsing',
|
||||
ProvenanceGenerated: 'provenancegenerated',
|
||||
TypeScriptGenerated: 'typescriptgenerated',
|
||||
WASMGenerated: 'wasmgenerated',
|
||||
} as const;
|
||||
export type SpecklCompilerCompilerPhase = typeof SpecklCompilerCompilerPhase[keyof typeof SpecklCompilerCompilerPhase];
|
||||
|
||||
export const SpecklCompilerArtifactKind = {
|
||||
CycloneDX: 'cyclonedx',
|
||||
Done: 'done',
|
||||
Provenance: 'provenance',
|
||||
SPDX: 'spdx',
|
||||
TypeScript: 'typescript',
|
||||
WebAssembly: 'webassembly',
|
||||
} as const;
|
||||
export type SpecklCompilerArtifactKind = typeof SpecklCompilerArtifactKind[keyof typeof SpecklCompilerArtifactKind];
|
||||
|
||||
export interface CompilationStarted {
|
||||
source: string;
|
||||
output: string;
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface PhaseCompleted {
|
||||
phase: SpecklCompilerCompilerPhase;
|
||||
durationMs: number;
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface ArtifactGenerated {
|
||||
kind: SpecklCompilerArtifactKind;
|
||||
path: string;
|
||||
bytes: number;
|
||||
at: string;
|
||||
}
|
||||
|
||||
export interface ErrorEmitted {
|
||||
phase: SpecklCompilerCompilerPhase;
|
||||
message: string;
|
||||
line: number;
|
||||
col: number;
|
||||
at: string;
|
||||
}
|
||||
|
||||
|
||||
export class SpecklCompilerMachine {
|
||||
phase: SpecklCompilerCompilerPhase;
|
||||
sourcePath: String;
|
||||
outputDir: String;
|
||||
ast: any;
|
||||
errors: String[];
|
||||
artifacts: any;
|
||||
decisionLog: CompilationStarted[] = [];
|
||||
|
||||
constructor() {
|
||||
this.phase = SpecklCompilerCompilerPhase.Idle;
|
||||
this.sourcePath = "";
|
||||
this.outputDir = "./out";
|
||||
this.ast = {};
|
||||
this.errors = [];
|
||||
this.artifacts = {};
|
||||
}
|
||||
|
||||
compile(sourcePath: string, outputDir: string) {
|
||||
if (!(this.phase === SpecklCompilerCompilerPhase.Idle)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.Idle');
|
||||
if (!(sourcePath.length > 0)) throw new Error('Guard failed: sourcePath.length > 0');
|
||||
this.decisionLog.push({
|
||||
source: sourcePath,
|
||||
output: outputDir,
|
||||
at: Date.now()
|
||||
} as CompilationStarted);
|
||||
}
|
||||
|
||||
parse() {
|
||||
if (!(this.phase === SpecklCompilerCompilerPhase.Parsing)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.Parsing');
|
||||
if (!(this.sourcePath.length > 0)) throw new Error('Guard failed: this.sourcePath.length > 0');
|
||||
this.decisionLog.push({
|
||||
phase: SpecklCompilerCompilerPhase.Parsing,
|
||||
durationMs: 0,
|
||||
at: Date.now()
|
||||
} as PhaseCompleted);
|
||||
}
|
||||
|
||||
generateProvenance() {
|
||||
if (!(this.phase === SpecklCompilerCompilerPhase.Parsed || this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.WASMGenerated)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.Parsed || this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.WASMGenerated');
|
||||
this.decisionLog.push({
|
||||
kind: SpecklCompilerCompilerPhase.Provenance,
|
||||
path: join(this.outputDir,
|
||||
bytes: 0,
|
||||
at: Date.now()
|
||||
} as ArtifactGenerated);
|
||||
}
|
||||
|
||||
generateBOMs() {
|
||||
if (!(this.phase === SpecklCompilerCompilerPhase.ProvenanceGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.ProvenanceGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed');
|
||||
this.decisionLog.push({
|
||||
kind: SpecklCompilerCompilerPhase.CycloneDX,
|
||||
path: join(this.outputDir,
|
||||
bytes: 0,
|
||||
at: Date.now()
|
||||
} as ArtifactGenerated);
|
||||
this.decisionLog.push({
|
||||
kind: SpecklCompilerCompilerPhase.SPDX,
|
||||
path: join(this.outputDir,
|
||||
bytes: 0,
|
||||
at: Date.now()
|
||||
} as ArtifactGenerated);
|
||||
}
|
||||
|
||||
generateTypeScript() {
|
||||
if (!(this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed');
|
||||
this.decisionLog.push({
|
||||
kind: SpecklCompilerCompilerPhase.TypeScript,
|
||||
path: join(this.outputDir,
|
||||
bytes: 0,
|
||||
at: Date.now()
|
||||
} as ArtifactGenerated);
|
||||
}
|
||||
|
||||
generateWASM() {
|
||||
if (!(this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.Parsed');
|
||||
this.decisionLog.push({
|
||||
kind: SpecklCompilerCompilerPhase.WebAssembly,
|
||||
path: join(this.outputDir,
|
||||
bytes: 0,
|
||||
at: Date.now()
|
||||
} as ArtifactGenerated);
|
||||
}
|
||||
|
||||
report() {
|
||||
if (!(this.phase === SpecklCompilerCompilerPhase.ProvenanceGenerated || this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.WASMGenerated)) throw new Error('Guard failed: this.phase === SpecklCompilerCompilerPhase.ProvenanceGenerated || this.phase === SpecklCompilerCompilerPhase.BOMsGenerated || this.phase === SpecklCompilerCompilerPhase.TypeScriptGenerated || this.phase === SpecklCompilerCompilerPhase.WASMGenerated');
|
||||
this.decisionLog.push({
|
||||
phase: SpecklCompilerCompilerPhase.Done,
|
||||
durationMs: 0,
|
||||
at: Date.now()
|
||||
} as PhaseCompleted);
|
||||
}
|
||||
|
||||
reset() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
151
compiler/out/speckl-compile/SpecklCompiler.wat
Normal file
151
compiler/out/speckl-compile/SpecklCompiler.wat
Normal file
|
|
@ -0,0 +1,151 @@
|
|||
;; Auto-generated by speckl-compile from SpecklCompiler
|
||||
;; Compile: wat2wasm SpecklCompiler.wat -o SpecklCompiler.wasm
|
||||
|
||||
(module
|
||||
;; Host imports for Map/Set/alloc
|
||||
(import "speckl" "map_create" (func $map_create (result i32)))
|
||||
(import "speckl" "map_set" (func $map_set (param i32 i32 i32)))
|
||||
(import "speckl" "map_get" (func $map_get (param i32 i32) (result i32)))
|
||||
(import "speckl" "map_has" (func $map_has (param i32 i32) (result i32)))
|
||||
(import "speckl" "map_remove" (func $map_remove (param i32 i32)))
|
||||
(import "speckl" "set_create" (func $set_create (result i32)))
|
||||
(import "speckl" "set_add" (func $set_add (param i32 i32)))
|
||||
(import "speckl" "set_has" (func $set_has (param i32 i32) (result i32)))
|
||||
(import "speckl" "set_size" (func $set_size (param i32) (result i32)))
|
||||
(import "speckl" "alloc" (func $alloc (param i32) (result i32)))
|
||||
(import "speckl" "write_i32" (func $write_i32 (param i32 i32)))
|
||||
(import "speckl" "write_f64" (func $write_f64 (param i32 f64)))
|
||||
|
||||
;; Linear memory (exported so host can read state)
|
||||
(memory (export "memory") 1)
|
||||
|
||||
;; State globals
|
||||
(global $phase (mut i32) (i32.const 0))
|
||||
(global $sourcePath (mut i32) (i32.const 0))
|
||||
(global $outputDir (mut i32) (i32.const 0))
|
||||
(global $ast (mut i32) (i32.const 0))
|
||||
(global $errors (mut i32) (i32.const 0))
|
||||
(global $artifacts (mut i32) (i32.const 0))
|
||||
|
||||
;; ---- init ----
|
||||
(func $init (export "init")
|
||||
(i32.const 0)
|
||||
(global.set $phase)
|
||||
(i32.const 0)
|
||||
(global.set $sourcePath)
|
||||
(i32.const 0)
|
||||
(global.set $outputDir)
|
||||
(i32.const 0)
|
||||
(global.set $ast)
|
||||
(i32.const 0)
|
||||
(global.set $errors)
|
||||
(i32.const 0)
|
||||
(global.set $artifacts)
|
||||
)
|
||||
|
||||
;; ---- Actions ----
|
||||
;; action: compile
|
||||
(func $compile (export "compile") (param $sourcePath i32) (param $outputDir i32) (result i32)
|
||||
|
||||
;; require: phase === Idle
|
||||
;; Complex right-hand side not compiled: phase === Idle
|
||||
;; require: len(sourcePath) > 0
|
||||
;; Complex guard not compiled: len(sourcePath) > 0
|
||||
;; emit CompilationStarted { ... }
|
||||
(i32.const 1)
|
||||
)
|
||||
|
||||
;; action: parse
|
||||
(func $parse (export "parse") (result i32)
|
||||
|
||||
;; require: phase === Parsing
|
||||
;; Complex right-hand side not compiled: phase === Parsing
|
||||
;; require: len(sourcePath) > 0
|
||||
;; Complex guard not compiled: len(sourcePath) > 0
|
||||
;; emit PhaseCompleted { ... }
|
||||
(i32.const 1)
|
||||
)
|
||||
|
||||
;; action: generateProvenance
|
||||
(func $generateProvenance (export "generateProvenance") (result i32)
|
||||
|
||||
;; require: phase === Parsed || phase === TypeScriptGenerated || phase === WASMGenerated
|
||||
;; Complex right-hand side not compiled: phase === Parsed || phase === TypeScriptGenerated || phase === WASMGenerated
|
||||
;; emit ArtifactGenerated { ... }
|
||||
(i32.const 1)
|
||||
)
|
||||
|
||||
;; action: generateBOMs
|
||||
(func $generateBOMs (export "generateBOMs") (result i32)
|
||||
|
||||
;; require: phase === ProvenanceGenerated || phase === Parsed
|
||||
;; Complex right-hand side not compiled: phase === ProvenanceGenerated || phase === Parsed
|
||||
;; emit ArtifactGenerated { ... }
|
||||
;; emit ArtifactGenerated { ... }
|
||||
(i32.const 1)
|
||||
)
|
||||
|
||||
;; action: generateTypeScript
|
||||
(func $generateTypeScript (export "generateTypeScript") (result i32)
|
||||
|
||||
;; require: phase === BOMsGenerated || phase === Parsed
|
||||
;; Complex right-hand side not compiled: phase === BOMsGenerated || phase === Parsed
|
||||
;; emit ArtifactGenerated { ... }
|
||||
(i32.const 1)
|
||||
)
|
||||
|
||||
;; action: generateWASM
|
||||
(func $generateWASM (export "generateWASM") (result i32)
|
||||
|
||||
;; require: phase === TypeScriptGenerated || phase === BOMsGenerated || phase === Parsed
|
||||
;; Complex right-hand side not compiled: phase === TypeScriptGenerated || phase === BOMsGenerated || phase === Parsed
|
||||
;; emit ArtifactGenerated { ... }
|
||||
(i32.const 1)
|
||||
)
|
||||
|
||||
;; action: report
|
||||
(func $report (export "report") (result i32)
|
||||
|
||||
;; require: phase === ProvenanceGenerated || phase === BOMsGenerated || phase === TypeScriptGenerated || phase === WASMGenerated
|
||||
;; Complex right-hand side not compiled: phase === ProvenanceGenerated || phase === BOMsGenerated || phase === TypeScriptGenerated || phase === WASMGenerated
|
||||
;; emit PhaseCompleted { ... }
|
||||
(i32.const 1)
|
||||
)
|
||||
|
||||
;; action: reset
|
||||
(func $reset (export "reset") (result i32)
|
||||
|
||||
(i32.const 1)
|
||||
)
|
||||
|
||||
;; ---- getState ----
|
||||
(func $getState (export "getState") (result i32 i32)
|
||||
(i32.const 0)
|
||||
(global.get $phase)
|
||||
(i32.store)
|
||||
(i32.const 4)
|
||||
(global.get $sourcePath)
|
||||
(i32.store)
|
||||
(i32.const 8)
|
||||
(global.get $outputDir)
|
||||
(i32.store)
|
||||
(i32.const 12)
|
||||
(global.get $ast)
|
||||
(i32.store)
|
||||
(i32.const 16)
|
||||
(global.get $errors)
|
||||
(i32.store)
|
||||
(i32.const 20)
|
||||
(global.get $artifacts)
|
||||
(i32.store)
|
||||
(i32.const 0)
|
||||
(i32.const 24)
|
||||
)
|
||||
|
||||
|
||||
;; checkConstraints — runtime constraint evaluation stub
|
||||
(func $checkConstraints (export "checkConstraints") (result i32)
|
||||
;; TODO: evaluate constraint expressions at runtime
|
||||
(i32.const 1)
|
||||
)
|
||||
)
|
||||
216
compiler/specs/speckl-compile.speckdl
Normal file
216
compiler/specs/speckl-compile.speckdl
Normal file
|
|
@ -0,0 +1,216 @@
|
|||
// speckl-compile.speckdl — The Speckl compiler, specified in SpeckDL
|
||||
// This is the ultimate dogfood: the compiler compiles itself.
|
||||
|
||||
speck SpecklCompiler {
|
||||
version: "0.4.0"
|
||||
author: "sscoble"
|
||||
license: "MIT"
|
||||
|
||||
// ─── Purpose ──────────────────────────────────────────────
|
||||
// Transform a .speckdl specification into provenance, BOMs,
|
||||
// and runnable artifacts (TypeScript, WebAssembly).
|
||||
|
||||
// ─── State ────────────────────────────────────────────────
|
||||
state: {
|
||||
phase: CompilerPhase,
|
||||
sourcePath: String,
|
||||
outputDir: String,
|
||||
ast: AST,
|
||||
errors: String[],
|
||||
artifacts: ArtifactMap
|
||||
}
|
||||
|
||||
init: {
|
||||
phase := Idle,
|
||||
sourcePath := "",
|
||||
outputDir := "./out",
|
||||
ast := {},
|
||||
errors := [],
|
||||
artifacts := {}
|
||||
}
|
||||
|
||||
// ─── Events ───────────────────────────────────────────────
|
||||
event CompilationStarted {
|
||||
source: String,
|
||||
output: String,
|
||||
at: String
|
||||
}
|
||||
|
||||
event PhaseCompleted {
|
||||
phase: CompilerPhase,
|
||||
durationMs: Nat,
|
||||
at: String
|
||||
}
|
||||
|
||||
event ArtifactGenerated {
|
||||
kind: ArtifactKind,
|
||||
path: String,
|
||||
bytes: Nat,
|
||||
at: String
|
||||
}
|
||||
|
||||
event ErrorEmitted {
|
||||
phase: CompilerPhase,
|
||||
message: String,
|
||||
line: Nat,
|
||||
col: Nat,
|
||||
at: String
|
||||
}
|
||||
|
||||
// ─── Actions ──────────────────────────────────────────────
|
||||
action compile(sourcePath: String, outputDir: String) {
|
||||
require phase === Idle
|
||||
require len(sourcePath) > 0
|
||||
|
||||
this.phase := Parsing
|
||||
this.sourcePath := sourcePath
|
||||
this.outputDir := outputDir
|
||||
this.errors := []
|
||||
|
||||
emit CompilationStarted { source: sourcePath, output: outputDir, at: now() }
|
||||
}
|
||||
|
||||
action parse() {
|
||||
require phase === Parsing
|
||||
require len(sourcePath) > 0
|
||||
|
||||
// Lexical analysis: tokenize .speckdl source
|
||||
// Syntactic analysis: build AST (SpeckNode, MemberNode hierarchy)
|
||||
// Semantic validation: type check, name resolution
|
||||
|
||||
this.phase := Parsed
|
||||
this.ast := parseSpeckFile(sourcePath)
|
||||
|
||||
emit PhaseCompleted { phase: Parsing, durationMs: 0, at: now() }
|
||||
}
|
||||
|
||||
action generateProvenance() {
|
||||
require phase === Parsed || phase === TypeScriptGenerated || phase === WASMGenerated
|
||||
|
||||
// Emit PROV-O JSON-LD provenance graph
|
||||
// Tracks: compiler version, source file, generation time, generator used
|
||||
|
||||
this.phase := ProvenanceGenerated
|
||||
|
||||
emit ArtifactGenerated { kind: Provenance, path: join(outputDir, ".prov.jsonld"), bytes: 0, at: now() }
|
||||
}
|
||||
|
||||
action generateBOMs() {
|
||||
require phase === ProvenanceGenerated || phase === Parsed
|
||||
|
||||
// Emit CycloneDX SBOM (software bill of materials)
|
||||
// Emit SPDX BOM (license and provenance)
|
||||
|
||||
this.phase := BOMsGenerated
|
||||
|
||||
emit ArtifactGenerated { kind: CycloneDX, path: join(outputDir, ".specbom.cdx.json"), bytes: 0, at: now() }
|
||||
emit ArtifactGenerated { kind: SPDX, path: join(outputDir, ".specbom.spdx.json"), bytes: 0, at: now() }
|
||||
}
|
||||
|
||||
action generateTypeScript() {
|
||||
require phase === BOMsGenerated || phase === Parsed
|
||||
|
||||
// Transform AST → TypeScript state machine class
|
||||
// Actions become methods, guards become if-throws,
|
||||
// state vars become properties, init becomes constructor
|
||||
|
||||
this.phase := TypeScriptGenerated
|
||||
|
||||
emit ArtifactGenerated { kind: TypeScript, path: join(outputDir, ".ts"), bytes: 0, at: now() }
|
||||
}
|
||||
|
||||
action generateWASM() {
|
||||
require phase === TypeScriptGenerated || phase === BOMsGenerated || phase === Parsed
|
||||
|
||||
// Transform AST → WebAssembly Text (WAT)
|
||||
// Scalar state → WASM globals, Map/Set → host imports
|
||||
// Actions become exports with parameter mapping
|
||||
|
||||
this.phase := WASMGenerated
|
||||
|
||||
emit ArtifactGenerated { kind: WebAssembly, path: join(outputDir, ".wat"), bytes: 0, at: now() }
|
||||
}
|
||||
|
||||
action report() {
|
||||
require phase === ProvenanceGenerated || phase === BOMsGenerated || phase === TypeScriptGenerated || phase === WASMGenerated
|
||||
|
||||
// Print summary: phases completed, artifacts emitted, errors found
|
||||
// Return exit code 0 if no errors, 1 otherwise
|
||||
|
||||
this.phase := Done
|
||||
|
||||
emit PhaseCompleted { phase: Done, durationMs: 0, at: now() }
|
||||
}
|
||||
|
||||
action reset() {
|
||||
// Return to Idle for next compilation
|
||||
this.phase := Idle
|
||||
this.sourcePath := ""
|
||||
this.outputDir := "./out"
|
||||
this.ast := {}
|
||||
this.errors := []
|
||||
this.artifacts := {}
|
||||
}
|
||||
|
||||
// ─── Constraints ──────────────────────────────────────────
|
||||
constraint C1: len(errors) === 0 || phase === Done
|
||||
// If there are errors, compilation must reach Done (with non-zero exit)
|
||||
|
||||
constraint C2: forall a in artifacts: a.bytes > 0
|
||||
// Every generated artifact must have content
|
||||
|
||||
constraint C3: phase === Idle || len(sourcePath) > 0
|
||||
// Non-idle phases require a source file
|
||||
|
||||
constraint C4: artifacts[TypeScript] exists → phase >= TypeScriptGenerated
|
||||
// TypeScript artifact implies the phase was reached
|
||||
|
||||
constraint C5: artifacts[WebAssembly] exists → phase >= WASMGenerated
|
||||
// WASM artifact implies the phase was reached
|
||||
|
||||
// ─── Temporal Verifications ───────────────────────────────
|
||||
verify V1: always(implies(phase === Parsing, eventually(phase === Parsed || len(errors) > 0)))
|
||||
// Parsing must complete or produce errors
|
||||
|
||||
verify V2: always(implies(phase === Parsed, eventually(phase === Done)))
|
||||
// Parsed must eventually reach Done
|
||||
|
||||
verify V3: always(implies(phase === Done, forall a in artifacts: a.bytes > 0 || a.kind === ErrorLog))
|
||||
// Done implies all non-error artifacts have content
|
||||
|
||||
// ─── Provenance ───────────────────────────────────────────
|
||||
provenance {
|
||||
wasGeneratedBy(speckl-compile, SpecklCompiler, "2026-05-03T19:00:00Z")
|
||||
used(SpecklCompiler, speckdl-v0.4, "grammar")
|
||||
used(SpecklCompiler, typescript-5.4, "codegen")
|
||||
used(SpecklCompiler, wasm-spec-2.0, "codegen")
|
||||
}
|
||||
|
||||
// ─── BOM ──────────────────────────────────────────────────
|
||||
bom {
|
||||
compiler: "speckl-compile@0.4.0"
|
||||
solver: "z3@4.13.0"
|
||||
runtime: "node@22.0.0"
|
||||
}
|
||||
|
||||
// ─── Supporting Types ───────────────────────────────────────
|
||||
// These would be defined in a standard library eventually
|
||||
|
||||
interface CompilerPhase {
|
||||
Idle, Parsing, Parsed, ProvenanceGenerated, BOMsGenerated,
|
||||
TypeScriptGenerated, WASMGenerated, Done
|
||||
}
|
||||
|
||||
interface ArtifactKind {
|
||||
Provenance, CycloneDX, SPDX, TypeScript, WebAssembly, ErrorLog
|
||||
}
|
||||
|
||||
interface AST {
|
||||
// Abstract syntax tree — opaque in this spec
|
||||
// Actual structure defined by parser implementation
|
||||
}
|
||||
|
||||
interface ArtifactMap {
|
||||
// Map from ArtifactKind to generated file metadata
|
||||
}
|
||||
}
|
||||
|
|
@ -41,19 +41,28 @@ function emitSpeck(speck: SpeckNode): string {
|
|||
const constraints = members.filter(m => m.type === 'constraint') as ConstraintNode[];
|
||||
const verifies = members.filter(m => m.type === 'verify') as VerifyNode[];
|
||||
|
||||
// Extract state enum values from an interface named *State if present
|
||||
const stateInterface = members.find(m => m.type === 'interface' && m.name.endsWith('State')) as any;
|
||||
// Collect ALL enum-like interfaces (interfaces with method names treated as enum values)
|
||||
const allInterfaces = members.filter(m => m.type === 'interface') as any[];
|
||||
const enumInterfaces = allInterfaces.filter(iface => iface?.methods?.length > 0);
|
||||
|
||||
// If no interfaces have methods, fall back to scanning action guards for capitalized words
|
||||
let stateValues: string[] = [];
|
||||
if (stateInterface?.methods?.length) {
|
||||
stateValues = stateInterface.methods.map((m: any) => m.name);
|
||||
if (enumInterfaces.length > 0) {
|
||||
for (const iface of enumInterfaces) {
|
||||
stateValues.push(...iface.methods.map((m: any) => m.name));
|
||||
}
|
||||
if (stateValues.length === 0) {
|
||||
} else {
|
||||
const seen = new Set<string>();
|
||||
const builtinTypes = new Set(['String', 'Nat', 'Int', 'Bool', 'Real', 'Bytes', 'Date', 'Number']);
|
||||
for (const a of actions) {
|
||||
for (const s of a.statements) {
|
||||
if (s.type === 'require') {
|
||||
const matches = s.expr.match(/\b(Open|Grooming|Groomed|Assigned|InProgress|InReview|Merged|Blocked|Closed)\b/g);
|
||||
if (matches) matches.forEach(m => seen.add(m));
|
||||
const matches = s.expr.match(/\b[A-Z][a-zA-Z]+\b/g);
|
||||
if (matches) {
|
||||
matches.forEach((m: string) => {
|
||||
if (!builtinTypes.has(m)) seen.add(m);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -61,16 +70,98 @@ function emitSpeck(speck: SpeckNode): string {
|
|||
}
|
||||
if (stateValues.length === 0) stateValues = ['Open', 'Closed'];
|
||||
|
||||
const stateEnumName = `${speck.name}State`;
|
||||
const stateEnum = `export const ${stateEnumName} = {\n` +
|
||||
stateValues.map((v: string) => ` ${v}: '${v.toLowerCase()}',`).join('\n') +
|
||||
// Build a map of interface name -> enum values for multi-enum specs
|
||||
const enumMap = new Map<string, string[]>();
|
||||
for (const iface of enumInterfaces) {
|
||||
enumMap.set(iface.name, iface.methods.map((m: any) => m.name));
|
||||
}
|
||||
|
||||
// Determine which enum name to use for state comparisons
|
||||
const stateVarType = stateNode?.variables?.[0]?.typeExpr;
|
||||
let stateEnumName = `${speck.name}State`;
|
||||
if (stateVarType?.type === 'ident' && stateVarType.name) {
|
||||
const cleanTypeName = cleanName(stateVarType.name);
|
||||
stateEnumName = `${speck.name}${cleanTypeName}`;
|
||||
} else if (enumMap.size > 0) {
|
||||
const firstEnum = enumMap.keys().next().value;
|
||||
if (firstEnum) stateEnumName = `${speck.name}${firstEnum}`;
|
||||
}
|
||||
|
||||
// If we fell back to guard scanning, create the primary enum under the right name
|
||||
if (enumInterfaces.length === 0) {
|
||||
enumMap.set(stateEnumName.replace(speck.name, ''), stateValues);
|
||||
}
|
||||
|
||||
// Scan event field types for secondary enum types (e.g., ArtifactKind)
|
||||
// If the type is an ident that matches an interface name, generate an enum for it
|
||||
const eventFieldTypes = new Set<string>();
|
||||
for (const ev of events) {
|
||||
for (const f of ev.fields) {
|
||||
if (f.type?.type === 'ident' && f.type.name) {
|
||||
const typeName = cleanName(f.type.name);
|
||||
if (typeName !== stateEnumName.replace(speck.name, '')) {
|
||||
eventFieldTypes.add(typeName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Add any event field types that match interface names as enums
|
||||
for (const iface of allInterfaces) {
|
||||
const ifaceName = cleanName(iface.name);
|
||||
if (eventFieldTypes.has(ifaceName)) {
|
||||
// If the interface has methods, use them; otherwise scan guards for values
|
||||
if (iface.methods?.length > 0) {
|
||||
enumMap.set(ifaceName, iface.methods.map((m: any) => m.name));
|
||||
} else {
|
||||
// Scan all action statements (both require guards and emit fields) for enum values
|
||||
const seen = new Set<string>();
|
||||
const builtinTypes = new Set(['String', 'Nat', 'Int', 'Bool', 'Real', 'Bytes', 'Date', 'Number']);
|
||||
for (const a of actions) {
|
||||
for (const s of a.statements) {
|
||||
if (s.type === 'require') {
|
||||
const matches = s.expr.match(/\b[A-Z][a-zA-Z]+\b/g);
|
||||
if (matches) {
|
||||
matches.forEach((m: string) => {
|
||||
if (!builtinTypes.has(m)) seen.add(m);
|
||||
});
|
||||
}
|
||||
}
|
||||
if (s.type === 'emit') {
|
||||
for (const f of s.fields) {
|
||||
const matches = f.value.match(/\b[A-Z][a-zA-Z]+\b/g);
|
||||
if (matches) {
|
||||
matches.forEach((m: string) => {
|
||||
if (!builtinTypes.has(m)) seen.add(m);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Remove values already assigned to the primary enum
|
||||
const primaryValues = new Set(enumMap.get(stateEnumName.replace(speck.name, '')) || []);
|
||||
const secondaryValues = Array.from(seen).filter(v => !primaryValues.has(v));
|
||||
if (secondaryValues.length > 0) {
|
||||
enumMap.set(ifaceName, secondaryValues.sort());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
const allEnums: string[] = [];
|
||||
for (const [enumName, values] of enumMap) {
|
||||
const fullEnumName = `${speck.name}${enumName}`;
|
||||
const enumDef = `export const ${fullEnumName} = {\n` +
|
||||
values.map((v: string) => ` ${v}: '${v.toLowerCase()}',`).join('\n') +
|
||||
'\n} as const;\n' +
|
||||
`export type ${stateEnumName} = typeof ${stateEnumName}[keyof typeof ${stateEnumName}];`;
|
||||
`export type ${fullEnumName} = typeof ${fullEnumName}[keyof typeof ${fullEnumName}];`;
|
||||
allEnums.push(enumDef);
|
||||
}
|
||||
const stateEnum = allEnums.join('\n\n');
|
||||
|
||||
const eventInterfaces = events.map(e => {
|
||||
const fields = e.fields.map(f => {
|
||||
const name = cleanName(f.name);
|
||||
return ` ${name}: ${tsType(f.type)};`;
|
||||
return ` ${name}: ${tsType(f.type, speck.name, enumMap)};`;
|
||||
}).join('\n');
|
||||
return `export interface ${e.name} {\n${fields}\n}`;
|
||||
}).join('\n\n');
|
||||
|
|
@ -85,20 +176,26 @@ function emitSpeck(speck: SpeckNode): string {
|
|||
const stateVarNames = new Set(stateVars.map(v => v.name));
|
||||
|
||||
const stateProperties = stateVars.map(v => {
|
||||
return ` ${v.name}: ${tsType(v.typeExpr)};`;
|
||||
return ` ${v.name}: ${tsType(v.typeExpr, speck.name, enumMap)};`;
|
||||
}).join('\n');
|
||||
|
||||
// Collect all known state values from all enums for the rewriter
|
||||
const allKnownStateValues: string[] = [];
|
||||
for (const values of enumMap.values()) {
|
||||
allKnownStateValues.push(...values);
|
||||
}
|
||||
|
||||
const initAssignments = initNode
|
||||
? initNode.assignments.map(a => {
|
||||
const name = cleanName(a.name);
|
||||
const expr = cleanExpr(a.expr);
|
||||
// Replace bare state literals with enum refs in init
|
||||
const enumRef = replaceStateWithEnum(expr, stateEnumName);
|
||||
const enumRef = replaceStateWithEnum(expr, stateEnumName, allKnownStateValues);
|
||||
return ` this.${name} = ${enumRef};`;
|
||||
}).join('\n')
|
||||
: '';
|
||||
|
||||
const actionMethods = actions.map(a => emitAction(a, stateVarNames, speck.name, stateEnumName)).join('\n\n');
|
||||
const actionMethods = actions.map(a => emitAction(a, stateVarNames, speck.name, stateEnumName, allKnownStateValues, enumMap)).join('\n\n');
|
||||
|
||||
const constraintChecks = constraints.map((c, i) =>
|
||||
` // C${i + 1}: ${c.expr}\n` +
|
||||
|
|
@ -118,11 +215,13 @@ function emitSpeck(speck: SpeckNode): string {
|
|||
` }`
|
||||
).join('\n\n');
|
||||
|
||||
// Decision log type: use first event if available, otherwise any[]
|
||||
const logType = events.length > 0 ? events[0].name + '[]' : 'any[]';
|
||||
|
||||
const classBody = `
|
||||
export class ${className} {
|
||||
${stateProperties}
|
||||
decisionLog: StateTransition[] = [];
|
||||
evidence: EvidenceItem[] = [];
|
||||
decisionLog: ${logType} = [];
|
||||
|
||||
constructor() {
|
||||
${initAssignments || ' // No init block defined'}
|
||||
|
|
@ -146,18 +245,18 @@ ${verifyChecks}
|
|||
return file;
|
||||
}
|
||||
|
||||
function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: string, stateEnumName: string): string {
|
||||
function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: string, stateEnumName: string, knownStateValues: string[], enumMap: Map<string, string[]>): string {
|
||||
// Collect local parameter names so we DON'T prefix them with this.
|
||||
const localNames = new Set(action.params.map(p => p.name));
|
||||
|
||||
const params = action.params
|
||||
.map(p => `${p.name}: ${tsType(p.type)}`)
|
||||
.map(p => `${p.name}: ${tsType(p.type, speckName, enumMap)}`)
|
||||
.join(', ');
|
||||
|
||||
const requires = action.statements
|
||||
.filter(s => s.type === 'require')
|
||||
.map(s => {
|
||||
const expr = rewriteExpr(s.expr, stateVarNames, localNames, stateEnumName);
|
||||
const expr = rewriteExpr(s.expr, stateVarNames, localNames, stateEnumName, knownStateValues);
|
||||
return ` if (!(${expr})) throw new Error('Guard failed: ${expr.replace(/'/g, "\\'")}');`;
|
||||
})
|
||||
.join('\n');
|
||||
|
|
@ -166,7 +265,7 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: s
|
|||
.filter(s => s.type === 'assign')
|
||||
.map(s => {
|
||||
const target = cleanName(s.target);
|
||||
const expr = rewriteExpr(s.expr, stateVarNames, localNames, stateEnumName);
|
||||
const expr = rewriteExpr(s.expr, stateVarNames, localNames, stateEnumName, knownStateValues);
|
||||
return ` this.${target} = ${expr};`;
|
||||
})
|
||||
.join('\n');
|
||||
|
|
@ -177,7 +276,7 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: s
|
|||
const fields = s.fields
|
||||
.map(f => {
|
||||
const name = cleanName(f.name);
|
||||
const val = rewriteExpr(f.value, stateVarNames, localNames, stateEnumName);
|
||||
const val = rewriteExpr(f.value, stateVarNames, localNames, stateEnumName, knownStateValues);
|
||||
return ` ${name}: ${val}`;
|
||||
})
|
||||
.join(',\n');
|
||||
|
|
@ -191,7 +290,7 @@ function emitAction(action: ActionNode, stateVarNames: Set<string>, speckName: s
|
|||
|
||||
// Rewrite SpeckDL expressions to valid TypeScript
|
||||
// Only prefix identifiers with `this.` if they are state variables AND not local params
|
||||
function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<string>, stateEnumName: string): string {
|
||||
function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<string>, stateEnumName: string, knownStateValues: string[]): string {
|
||||
function shouldPrefix(id: string): boolean {
|
||||
return stateVarNames.has(id) && !localNames.has(id);
|
||||
}
|
||||
|
|
@ -204,7 +303,13 @@ function rewriteExpr(expr: string, stateVarNames: Set<string>, localNames: Set<s
|
|||
.replace(/\bforall\s+(\w+)\s+in\s+([^:]+):\s*(.+)/g, '$2.every(($1) => $3)');
|
||||
|
||||
// Step 2: Replace state enum literals with enum refs BEFORE identifier replacement
|
||||
ts = ts.replace(/\b(Open|Grooming|Groomed|Assigned|InProgress|InReview|Merged|Blocked|Closed)\b/g, `${stateEnumName}.$1`);
|
||||
const allStateValues = new Set<string>(knownStateValues);
|
||||
// Add common fallback states
|
||||
['Open','Grooming','Groomed','Assigned','InProgress','InReview','Merged','Blocked','Closed','Idle','Parsing','Parsed','ProvenanceGenerated','BOMsGenerated','TypeScriptGenerated','WASMGenerated','Done'].forEach(s => allStateValues.add(s));
|
||||
if (allStateValues.size > 0) {
|
||||
const stateValueRegex = new RegExp('\\b(' + Array.from(allStateValues).join('|') + ')\\b', 'g');
|
||||
ts = ts.replace(stateValueRegex, `${stateEnumName}.$1`);
|
||||
}
|
||||
|
||||
// Step 3: Replace identifiers that are state variables but not local params
|
||||
const tokens = ts.split(/([\s\(\)\[\]{}+\-*/=<>!&|.,;:'"`]+)/);
|
||||
|
|
@ -254,15 +359,19 @@ function cleanName(s: string): string {
|
|||
return s.replace(/,$/, '').trim();
|
||||
}
|
||||
|
||||
function replaceStateWithEnum(expr: string, enumName: string): string {
|
||||
return expr.replace(/\b(Open|Grooming|Groomed|Assigned|InProgress|InReview|Merged|Blocked|Closed)\b/g, `${enumName}.$1`);
|
||||
function replaceStateWithEnum(expr: string, enumName: string, knownStateValues: string[]): string {
|
||||
const allStateValues = new Set<string>(knownStateValues);
|
||||
['Open','Grooming','Groomed','Assigned','InProgress','InReview','Merged','Blocked','Closed','Idle','Parsing','Parsed','ProvenanceGenerated','BOMsGenerated','TypeScriptGenerated','WASMGenerated','Done'].forEach(s => allStateValues.add(s));
|
||||
if (allStateValues.size === 0) return expr;
|
||||
const stateValueRegex = new RegExp('\\b(' + Array.from(allStateValues).join('|') + ')\\b', 'g');
|
||||
return expr.replace(stateValueRegex, `${enumName}.$1`);
|
||||
}
|
||||
|
||||
function cleanExpr(s: string): string {
|
||||
return s.replace(/,$/, '').trim();
|
||||
}
|
||||
|
||||
function tsType(t: any): string {
|
||||
function tsType(t: any, speckName: string = '', enumMap?: Map<string, string[]>): string {
|
||||
if (!t) return 'unknown';
|
||||
if (t.type === 'primitive') {
|
||||
const map: Record<string, string> = {
|
||||
|
|
@ -271,22 +380,28 @@ function tsType(t: any): string {
|
|||
};
|
||||
return map[t.name] || t.name.toLowerCase();
|
||||
}
|
||||
if (t.type === 'list') return `${tsType(t.elementType)}[]`;
|
||||
if (t.type === 'set') return `Set<${tsType(t.elementType)}>`;
|
||||
if (t.type === 'map') return `Map<${tsType(t.keyType)}, ${tsType(t.valueType)}>`;
|
||||
if (t.type === 'list') return `${tsType(t.elementType, speckName, enumMap)}[]`;
|
||||
if (t.type === 'set') return `Set<${tsType(t.elementType, speckName, enumMap)}>`;
|
||||
if (t.type === 'map') return `Map<${tsType(t.keyType, speckName, enumMap)}, ${tsType(t.valueType, speckName, enumMap)}>`;
|
||||
if (t.type === 'record') {
|
||||
const fields = t.fields.map((f: any) => {
|
||||
const name = cleanName(f.name);
|
||||
return `${name}: ${tsType(f.type)}`;
|
||||
return `${name}: ${tsType(f.type, speckName, enumMap)}`;
|
||||
}).join('; ');
|
||||
return `{ ${fields} }`;
|
||||
}
|
||||
if (t.type === 'ident') {
|
||||
const name = cleanName(t.name);
|
||||
// Map known identifier types
|
||||
if (name === 'IssueState') return 'IssueWorkflowState';
|
||||
if (name === 'Nat') return 'number';
|
||||
return name;
|
||||
const rawName = cleanName(t.name); // strip trailing comma
|
||||
// Check if this ident maps to a known enum interface
|
||||
if (enumMap && enumMap.has(rawName)) {
|
||||
return `${speckName}${rawName}`;
|
||||
}
|
||||
// Map known identifier types to concrete TS types
|
||||
if (rawName === 'IssueState') return 'IssueWorkflowState';
|
||||
if (rawName === 'Nat') return 'number';
|
||||
// Opaque interfaces become 'any' for now
|
||||
if (rawName === 'AST' || rawName === 'ArtifactMap') return 'any';
|
||||
return rawName;
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"@type": "prov:Generation",
|
||||
"prov:activity": {
|
||||
"@type": "prov:Activity",
|
||||
"prov:startTime": "2026-05-03T18:51:34.102Z"
|
||||
"prov:startTime": "2026-05-03T20:13:42.864Z"
|
||||
}
|
||||
},
|
||||
"rdfs:label": "RetryHandler",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
"name": "Scott Scoble",
|
||||
"email": "scott@scoble.me"
|
||||
},
|
||||
"timestamp": "2026-05-03T18:51:34.104Z"
|
||||
"timestamp": "2026-05-03T20:13:42.867Z"
|
||||
},
|
||||
{
|
||||
"subject": "speck:RetryHandler",
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
"name": "Speckl Compiler",
|
||||
"email": "compiler@speckl.scoble.me"
|
||||
},
|
||||
"timestamp": "2026-05-03T18:51:34.104Z"
|
||||
"timestamp": "2026-05-03T20:13:42.867Z"
|
||||
},
|
||||
{
|
||||
"subject": "speck:RetryHandler",
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
"name": "Speckl Compiler",
|
||||
"email": "compiler@speckl.scoble.me"
|
||||
},
|
||||
"timestamp": "2026-05-03T18:51:34.104Z"
|
||||
"timestamp": "2026-05-03T20:13:42.867Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"spdxVersion": "3.0.1",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"creationInfo": {
|
||||
"created": "2026-05-03T18:51:34.105Z",
|
||||
"created": "2026-05-03T20:13:42.868Z",
|
||||
"createdBy": [
|
||||
"Organization: Speckl",
|
||||
"tool: speckl-compile-0.2.0"
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"@type": "spdx:Tool",
|
||||
"name": "speckl-compile"
|
||||
},
|
||||
"timestamp": "2026-05-03T18:51:34.105Z",
|
||||
"timestamp": "2026-05-03T20:13:42.868Z",
|
||||
"comment": "Review type: manual"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue