fix: test path resolution — use __dirname instead of process.cwd() (cycle 74)
This commit is contained in:
parent
fa6f5bcf21
commit
857fa6ceb4
17 changed files with 430 additions and 416 deletions
|
|
@ -1,6 +1,10 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { parseSpeckContent, parseSpeckFile } from '../src/parser.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('SpeckDL Parser', () => {
|
||||
describe('parseSpeckContent', () => {
|
||||
it('should parse a minimal speck definition with name', () => {
|
||||
|
|
@ -533,7 +537,7 @@ speck Second {
|
|||
});
|
||||
|
||||
it('should parse full TigerBeetle speck from file', () => {
|
||||
const ast = parseSpeckFile('./__tests__/tigerbeetle.speck');
|
||||
const ast = parseSpeckFile(path.join(__dirname, 'tigerbeetle.speck'));
|
||||
expect(ast.specks).toHaveLength(1);
|
||||
|
||||
const speck = ast.specks[0];
|
||||
|
|
|
|||
165
compiler/dist/__tests__/compiler.test.js
vendored
165
compiler/dist/__tests__/compiler.test.js
vendored
|
|
@ -1,10 +1,13 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
import { parseSpeckFile } from '../parser.js';
|
||||
import { generateProvenanceGraph } from '../generators/provenance.js';
|
||||
import { generateCycloneDX } from '../generators/cyclonedx.js';
|
||||
import { generateSPDX } from '../generators/spdx.js';
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
describe('Compiler E2E', () => {
|
||||
let ast;
|
||||
const testOutputDir = './test-output';
|
||||
|
|
@ -15,7 +18,7 @@ describe('Compiler E2E', () => {
|
|||
}
|
||||
});
|
||||
it('should parse RetryHandler.speck', () => {
|
||||
const specPath = path.join(process.cwd(), '..', 'examples', 'RetryHandler.speck');
|
||||
const specPath = path.join(__dirname, '..', '..', '..', 'examples', 'RetryHandler.speck');
|
||||
ast = parseSpeckFile(specPath);
|
||||
expect(ast).toBeDefined();
|
||||
expect(ast.specks).toBeDefined();
|
||||
|
|
@ -54,4 +57,164 @@ describe('Compiler E2E', () => {
|
|||
expect(spdx.spdx).toBeDefined();
|
||||
});
|
||||
});
|
||||
// ---------------------------------------------------------------------------
|
||||
// Regression tests for Z3 / TypeScript / Rust expression translation fixes.
|
||||
//
|
||||
// These tests compile the FederatedMeetupProduct spec against each backend
|
||||
// (z3, typescript, rust) and assert that the generated output contains the
|
||||
// expected translated patterns. They guard against regressions in:
|
||||
// - implies() argument preservation (Z3 + TS + Rust)
|
||||
// - multi-level field access flattening (Z3)
|
||||
// - len()/length() normalization (Z3)
|
||||
// - nil → speckl_nil translation (Z3)
|
||||
// - != → (distinct X Y) wrapping (Z3)
|
||||
// - constraint checks emitting real every() / iter().all() (TS + Rust)
|
||||
// - 'in' operator → .has() in verify expressions (TS)
|
||||
// - multiple verify blocks all emitted (memberKey dedup fix) (TS + Rust)
|
||||
// ---------------------------------------------------------------------------
|
||||
const SPEC_PATH = '/home/sscoble/projects/federated-meetup/specs/FederatedMeetupProduct.speckdl';
|
||||
const OUT_DIR = './out'; // CLI writes to ./out relative to compiler cwd
|
||||
const COMPILER_DIR = '/home/sscoble/speckl/compiler';
|
||||
describe('Z3 / TS / Rust expression translation regressions', () => {
|
||||
let z3Output;
|
||||
let tsOutput;
|
||||
let rustOutput;
|
||||
beforeAll(() => {
|
||||
// Clean out directory to avoid stale output
|
||||
execSync(`rm -rf ${OUT_DIR}`, { cwd: COMPILER_DIR });
|
||||
execSync(`mkdir -p ${OUT_DIR}`, { cwd: COMPILER_DIR });
|
||||
// Compile to all three targets
|
||||
execSync(`npx tsx src/index.ts ${SPEC_PATH} --target z3 --output ${OUT_DIR}`, { cwd: COMPILER_DIR, stdio: 'pipe' });
|
||||
z3Output = fs.readFileSync(path.join(COMPILER_DIR, OUT_DIR, 'FederatedMeetupProduct.smt2'), 'utf-8');
|
||||
execSync(`npx tsx src/index.ts ${SPEC_PATH} --target typescript --output ${OUT_DIR}`, { cwd: COMPILER_DIR, stdio: 'pipe' });
|
||||
tsOutput = fs.readFileSync(path.join(COMPILER_DIR, OUT_DIR, 'FederatedMeetupProduct.ts'), 'utf-8');
|
||||
execSync(`npx tsx src/index.ts ${SPEC_PATH} --target rust --output ${OUT_DIR}`, { cwd: COMPILER_DIR, stdio: 'pipe' });
|
||||
rustOutput = fs.readFileSync(path.join(COMPILER_DIR, OUT_DIR, 'FederatedMeetupProduct', 'src', 'lib.rs'), 'utf-8');
|
||||
}, 120_000);
|
||||
// 1. Z3: implies() preserves both arguments → (=> P Q) not (=> true true)
|
||||
it('Z3: implies() emits (=> P Q) preserving both arguments', () => {
|
||||
expect(z3Output).toContain('(=>');
|
||||
expect(z3Output).toContain('(=> (= o_status Refunded) (distinct o_refunded_at speckl_nil))');
|
||||
// Ensure we did NOT regress to (=> true true)
|
||||
expect(z3Output).not.toContain('(=> true true)');
|
||||
expect(z3Output).not.toContain('(=> true');
|
||||
});
|
||||
// 2. Z3: multi-level field access m.price.amount → m_price_amount
|
||||
it('Z3: multi-level field access m.price.amount flattens to m_price_amount', () => {
|
||||
expect(z3Output).toContain('m_price_amount');
|
||||
// Ensure we did NOT regress to single-level m_price or bare m.price.amount
|
||||
expect(z3Output).not.toContain('m.price.amount');
|
||||
// The constraint m_price_amount >= 0 should be present
|
||||
expect(z3Output).toContain('(>= m_price_amount 0)');
|
||||
});
|
||||
// 3. Z3: len() handled same as length() → speckl_len_Int
|
||||
it('Z3: len() is translated to speckl_len_Int (same as length())', () => {
|
||||
// The spec uses len(m.price.currency) — should become speckl_len_Int
|
||||
expect(z3Output).toContain('(speckl_len_Int m_price_currency)');
|
||||
// Ensure we did NOT regress to bare len() or length()
|
||||
expect(z3Output).not.toMatch(/\blen\s*\(/);
|
||||
expect(z3Output).not.toMatch(/\blength\s*\(/);
|
||||
});
|
||||
// 4. Z3: nil translated to speckl_nil
|
||||
it('Z3: nil is translated to speckl_nil sentinel', () => {
|
||||
expect(z3Output).toContain('(declare-const speckl_nil Int)');
|
||||
expect(z3Output).toContain('speckl_nil');
|
||||
// Ensure we did NOT regress to bare nil / null
|
||||
expect(z3Output).not.toMatch(/\bnil\b/);
|
||||
});
|
||||
// 5. Z3: != produces (distinct X Y) not bare 'distinct'
|
||||
it('Z3: != produces wrapped (distinct X Y) not bare distinct', () => {
|
||||
// The constraint g.hosting_tier != Free should emit (distinct g_hosting_tier Free)
|
||||
expect(z3Output).toContain('(distinct g_hosting_tier Free)');
|
||||
// Ensure we did NOT regress to bare 'distinct g_hosting_tier Free' without parens
|
||||
// (the fix wraps the operator in parens)
|
||||
// A bare distinct would look like "g_hosting_tier distinct Free" without surrounding parens
|
||||
expect(z3Output).not.toMatch(/[^(]\w+\s+distinct\s+\w+/);
|
||||
});
|
||||
// 6. TS: implies(p,q) → (!p || q) not (p || q)
|
||||
it('TS: implies(p, q) translates to (!p || q), not (p || q)', () => {
|
||||
// constraint 6: implies(o.status == Refunded, o.refunded_at != nil)
|
||||
// Should become (!(o.status === ...) || (o.refunded_at !== ...))
|
||||
expect(tsOutput).toContain('(!(');
|
||||
expect(tsOutput).toContain('||');
|
||||
// The specific pattern: (!(o.status === ... || (o.refunded_at !== nil))
|
||||
expect(tsOutput).toMatch(/!\(o\.status === .* \|\| \(o\.refunded_at !== /);
|
||||
// Ensure the CODE (not comments) has negation on p.
|
||||
// A regression would have `return this.orders.every((o: any) => (o.status ===`
|
||||
// (without the `!(` negation before `o.status`).
|
||||
// Strip comments to avoid matching the original spec text in comments.
|
||||
const tsNoComments = tsOutput.replace(/^\s*\/\/.*$/gm, '');
|
||||
// Positive: the every() body starts with (!(o.status — the (! is the negation
|
||||
expect(tsNoComments).toMatch(/every\(\(o:\s*any\)\s*=>\s*\(!\(o\.status/);
|
||||
// Negative: a regression would have every((o: any) => (o.status without the ! negation
|
||||
expect(tsNoComments).not.toMatch(/every\(\(o:\s*any\)\s*=>\s*\(o\.status/);
|
||||
});
|
||||
// 7. TS: constraint checks emit real every() not return true
|
||||
it('TS: constraint checks emit real .every() calls, not return true', () => {
|
||||
expect(tsOutput).toContain('.every(');
|
||||
// Multiple constraints use every() — at least 8 constraints
|
||||
const everyCount = (tsOutput.match(/\.every\(/g) || []).length;
|
||||
expect(everyCount).toBeGreaterThanOrEqual(8);
|
||||
// Ensure we did NOT regress to `return true` stubs
|
||||
// A stub would have checkConstraintN() { return true; }
|
||||
expect(tsOutput).not.toMatch(/checkConstraint\d+\(\):\s*boolean\s*\{\s*return\s*true\s*;\s*\}/);
|
||||
});
|
||||
// 8. TS: 'in' operator in verify expressions → .has() not raw 'in'
|
||||
it('TS: "in" operator in verify expressions translates to .has(), not raw in', () => {
|
||||
// V1: always(implies(event == PurchaseTicket, ticket_id in tickets))
|
||||
// Should become tickets.has(ticket_id)
|
||||
expect(tsOutput).toContain('.has(');
|
||||
expect(tsOutput).toContain('tickets.has(ticket_id)');
|
||||
// Ensure we did NOT regress to raw 'in' operator (JS `in` checks property keys, not membership).
|
||||
// A regression would look like `ticket_id in tickets` in CODE (not comments).
|
||||
// Strip comments to avoid matching the original spec text in comments.
|
||||
const tsNoComments = tsOutput.replace(/^\s*\/\/.*$/gm, '');
|
||||
expect(tsNoComments).not.toMatch(/\bticket_id\s+in\s+tickets\b/);
|
||||
});
|
||||
// 9. TS/Rust: multiple verify blocks all emitted (memberKey dedup fix)
|
||||
it('TS: all 6 verify blocks are emitted (memberKey dedup fix)', () => {
|
||||
// The spec has 6 verify blocks; previously dedup killed all but the first
|
||||
expect(tsOutput).toContain('verify1');
|
||||
expect(tsOutput).toContain('verify2');
|
||||
expect(tsOutput).toContain('verify3');
|
||||
expect(tsOutput).toContain('verify4');
|
||||
expect(tsOutput).toContain('verify5');
|
||||
expect(tsOutput).toContain('verify6');
|
||||
expect(tsOutput).not.toContain('verify7'); // only 6 verify blocks in spec
|
||||
});
|
||||
it('Rust: all 6 verify blocks are emitted (memberKey dedup fix)', () => {
|
||||
expect(rustOutput).toContain('verify_1');
|
||||
expect(rustOutput).toContain('verify_2');
|
||||
expect(rustOutput).toContain('verify_3');
|
||||
expect(rustOutput).toContain('verify_4');
|
||||
expect(rustOutput).toContain('verify_5');
|
||||
expect(rustOutput).toContain('verify_6');
|
||||
expect(rustOutput).not.toContain('verify_7');
|
||||
});
|
||||
// 10. Rust: constraint checks emit iter().all() not return true
|
||||
it('Rust: constraint checks emit iter().all(), not return true stubs', () => {
|
||||
expect(rustOutput).toContain('.iter().all(');
|
||||
// Multiple constraints use iter().all() — at least 8 constraints
|
||||
const iterAllCount = (rustOutput.match(/\.iter\(\)\.all\(/g) || []).length;
|
||||
expect(iterAllCount).toBeGreaterThanOrEqual(8);
|
||||
// Ensure we did NOT regress to `return true` stubs
|
||||
expect(rustOutput).not.toMatch(/check_invariant_\d+\(&self\)\s*->\s*bool\s*\{\s*true\s*\}/);
|
||||
});
|
||||
// 11. Rust: implies() → (!p || q) in Rust
|
||||
it('Rust: implies(p, q) translates to (!p || q) in Rust', () => {
|
||||
// constraint 6: implies(o.status == Refunded, o.refunded_at != nil)
|
||||
// Should become (!(o.status == ... || (o.refunded_at != ...))
|
||||
expect(rustOutput).toContain('(!(');
|
||||
expect(rustOutput).toMatch(/!\(o\.status == .* \|\| \(o\.refunded_at != /);
|
||||
// Ensure the CODE (not comments) has negation on p.
|
||||
// A regression would have `self.orders.iter().all(|o| (o.status ==`
|
||||
// (without the `!(` negation before `o.status`).
|
||||
// Strip comments to avoid matching the original spec text in comments.
|
||||
const rustNoComments = rustOutput.replace(/^\s*\/\/.*$/gm, '');
|
||||
// Positive: the iter().all() body starts with (!(o.status — the (! is the negation
|
||||
expect(rustNoComments).toMatch(/iter\(\)\.all\(\|o\|\s*\(!\(o\.status/);
|
||||
// Negative: a regression would have iter().all(|o| (o.status without the ! negation
|
||||
expect(rustNoComments).not.toMatch(/iter\(\)\.all\(\|o\|\s*\(o\.status/);
|
||||
});
|
||||
});
|
||||
//# sourceMappingURL=compiler.test.js.map
|
||||
2
compiler/dist/__tests__/compiler.test.js.map
vendored
2
compiler/dist/__tests__/compiler.test.js.map
vendored
File diff suppressed because one or more lines are too long
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":54.53141900000003,"failed":false}],[":src/__tests__/compiler.test.ts",{"duration":15.892710999999963,"failed":false}]]}
|
||||
{"version":"2.1.9","results":[[":__tests__/parser.test.ts",{"duration":55.17420900000002,"failed":false}],[":src/__tests__/compiler.test.ts",{"duration":3350.1431570000004,"failed":false}]]}
|
||||
|
|
@ -1,29 +0,0 @@
|
|||
{
|
||||
"@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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
{
|
||||
"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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
{
|
||||
"@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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,153 +0,0 @@
|
|||
// 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() {
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
;; 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)
|
||||
)
|
||||
)
|
||||
|
|
@ -1,12 +1,15 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { execSync } from 'child_process';
|
||||
import { parseSpeckFile } from '../parser.js';
|
||||
import { generateProvenanceGraph } from '../generators/provenance.js';
|
||||
import { generateCycloneDX } from '../generators/cyclonedx.js';
|
||||
import { generateSPDX } from '../generators/spdx.js';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
describe('Compiler E2E', () => {
|
||||
let ast: any;
|
||||
const testOutputDir = './test-output';
|
||||
|
|
@ -19,7 +22,7 @@ describe('Compiler E2E', () => {
|
|||
});
|
||||
|
||||
it('should parse RetryHandler.speck', () => {
|
||||
const specPath = path.join(process.cwd(), '..', 'examples', 'RetryHandler.speck');
|
||||
const specPath = path.join(__dirname, '..', '..', '..', 'examples', 'RetryHandler.speck');
|
||||
ast = parseSpeckFile(specPath);
|
||||
|
||||
expect(ast).toBeDefined();
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@
|
|||
"@type": "prov:Generation",
|
||||
"prov:activity": {
|
||||
"@type": "prov:Activity",
|
||||
"prov:startTime": "2026-06-30T00:31:19.971Z"
|
||||
"prov:startTime": "2026-06-30T01:54:05.948Z"
|
||||
}
|
||||
},
|
||||
"rdfs:label": "RetryHandler",
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
"name": "Scott Scoble",
|
||||
"email": "scott@scoble.me"
|
||||
},
|
||||
"timestamp": "2026-06-30T00:31:19.973Z"
|
||||
"timestamp": "2026-06-30T01:54:05.951Z"
|
||||
},
|
||||
{
|
||||
"subject": "speck:RetryHandler",
|
||||
|
|
@ -53,7 +53,7 @@
|
|||
"name": "Speckl Compiler",
|
||||
"email": "compiler@speckl.scoble.me"
|
||||
},
|
||||
"timestamp": "2026-06-30T00:31:19.974Z"
|
||||
"timestamp": "2026-06-30T01:54:05.951Z"
|
||||
},
|
||||
{
|
||||
"subject": "speck:RetryHandler",
|
||||
|
|
@ -68,7 +68,7 @@
|
|||
"name": "Speckl Compiler",
|
||||
"email": "compiler@speckl.scoble.me"
|
||||
},
|
||||
"timestamp": "2026-06-30T00:31:19.974Z"
|
||||
"timestamp": "2026-06-30T01:54:05.951Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
"spdxVersion": "3.0.1",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"creationInfo": {
|
||||
"created": "2026-06-30T00:31:19.976Z",
|
||||
"created": "2026-06-30T01:54:05.952Z",
|
||||
"createdBy": [
|
||||
"Organization: Speckl",
|
||||
"tool: speckl-compile-0.2.0"
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
"@type": "spdx:Tool",
|
||||
"name": "speckl-compile"
|
||||
},
|
||||
"timestamp": "2026-06-30T00:31:19.976Z",
|
||||
"timestamp": "2026-06-30T01:54:05.952Z",
|
||||
"comment": "Review type: manual"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
1
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
generated
vendored
Normal file
1
node_modules/.vite/vitest/da39a3ee5e6b4b0d3255bfef95601890afd80709/results.json
generated
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"version":"4.1.9","results":[[":compiler/dist/__tests__/compiler.test.js",{"duration":21.732912999999996,"failed":false}],[":compiler/__tests__/parser.test.ts",{"duration":56.655685000000005,"failed":false}],[":compiler/src/__tests__/compiler.test.ts",{"duration":3388.7825,"failed":false}]]}
|
||||
61
test-output/RetryHandler.prov.jsonld
Normal file
61
test-output/RetryHandler.prov.jsonld
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
{
|
||||
"@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:RetryHandler",
|
||||
"prov:qualifiedGeneration": {
|
||||
"@type": "prov:Generation",
|
||||
"prov:activity": {
|
||||
"@type": "prov:Activity",
|
||||
"prov:startTime": "2026-06-30T02:09:28.187Z"
|
||||
}
|
||||
},
|
||||
"rdfs:label": "RetryHandler",
|
||||
"prov:wasDerivedFrom": {
|
||||
"@id": "speck:FaultHandler"
|
||||
},
|
||||
"rdfs:comment": "specialization for retry logic",
|
||||
"speckl:satisfies": {
|
||||
"@id": "requirement:RESILIENCE-REQ-01"
|
||||
},
|
||||
"prov:wasAttributedTo": {
|
||||
"@id": "agent:Scott Scoble"
|
||||
},
|
||||
"prov:wasInformedBy": {
|
||||
"@id": "source:architecture_review:AR-2024-0215"
|
||||
}
|
||||
},
|
||||
{
|
||||
"@type": "prov:Entity",
|
||||
"@id": "speck:FaultHandler"
|
||||
},
|
||||
{
|
||||
"@type": "prov:Entity",
|
||||
"@id": "requirement:RESILIENCE-REQ-01",
|
||||
"rdfs:label": "RESILIENCE-REQ-01",
|
||||
"speckl:clause": "3.1.2"
|
||||
},
|
||||
{
|
||||
"@type": "prov:Entity",
|
||||
"@id": "requirement:RESILIENCE-REQ-02",
|
||||
"rdfs:label": "RESILIENCE-REQ-02"
|
||||
},
|
||||
{
|
||||
"@type": "prov:Agent",
|
||||
"@id": "agent:Scott Scoble",
|
||||
"rdfs:label": "Scott Scoble",
|
||||
"foaf:email": "scott@scoble.me"
|
||||
},
|
||||
{
|
||||
"@type": "prov:Activity",
|
||||
"@id": "source:architecture_review:AR-2024-0215",
|
||||
"prov:type": "architecture_review",
|
||||
"rdfs:label": "architecture_review - AR-2024-0215"
|
||||
}
|
||||
]
|
||||
}
|
||||
74
test-output/RetryHandler.specbom.cdx.json
Normal file
74
test-output/RetryHandler.specbom.cdx.json
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
{
|
||||
"bomFormat": "CycloneDX",
|
||||
"specVersion": "1.6",
|
||||
"components": [],
|
||||
"metadata": {
|
||||
"tools": [],
|
||||
"component": {
|
||||
"type": "application",
|
||||
"name": "RetryHandler",
|
||||
"version": "0.2.0",
|
||||
"properties": [
|
||||
{
|
||||
"name": "speckl:derivesFrom",
|
||||
"value": "FaultHandler"
|
||||
},
|
||||
{
|
||||
"name": "speckl:satisfies",
|
||||
"value": "RESILIENCE-REQ-01 clause 3.1.2"
|
||||
},
|
||||
{
|
||||
"name": "speckl:satisfies",
|
||||
"value": "RESILIENCE-REQ-02"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"annotations": [
|
||||
{
|
||||
"subject": "speck:RetryHandler",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "tag",
|
||||
"value": "provenanceType",
|
||||
"text": "author"
|
||||
}
|
||||
],
|
||||
"author": {
|
||||
"name": "Scott Scoble",
|
||||
"email": "scott@scoble.me"
|
||||
},
|
||||
"timestamp": "2026-06-30T02:09:28.192Z"
|
||||
},
|
||||
{
|
||||
"subject": "speck:RetryHandler",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "tag",
|
||||
"value": "provenanceType",
|
||||
"text": "source"
|
||||
}
|
||||
],
|
||||
"author": {
|
||||
"name": "Speckl Compiler",
|
||||
"email": "compiler@speckl.scoble.me"
|
||||
},
|
||||
"timestamp": "2026-06-30T02:09:28.192Z"
|
||||
},
|
||||
{
|
||||
"subject": "speck:RetryHandler",
|
||||
"annotations": [
|
||||
{
|
||||
"type": "tag",
|
||||
"value": "reviewType",
|
||||
"text": "manual"
|
||||
}
|
||||
],
|
||||
"author": {
|
||||
"name": "Speckl Compiler",
|
||||
"email": "compiler@speckl.scoble.me"
|
||||
},
|
||||
"timestamp": "2026-06-30T02:09:28.192Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
113
test-output/RetryHandler.specbom.spdx.json
Normal file
113
test-output/RetryHandler.specbom.spdx.json
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
{
|
||||
"@context": [
|
||||
"https://spdx.github.io/spdx-v3/spdx-context.jsonld",
|
||||
{
|
||||
"speckl": "https://speckl.scoble.me/ns/v0.2#"
|
||||
}
|
||||
],
|
||||
"@type": "spdx:Document",
|
||||
"spdx": {
|
||||
"@id": "spdx:RetryHandler",
|
||||
"@type": "spdx:Snippet",
|
||||
"name": "RetryHandler",
|
||||
"spdxVersion": "3.0.1",
|
||||
"dataLicense": "CC0-1.0",
|
||||
"creationInfo": {
|
||||
"created": "2026-06-30T02:09:28.194Z",
|
||||
"createdBy": [
|
||||
"Organization: Speckl",
|
||||
"tool: speckl-compile-0.2.0"
|
||||
]
|
||||
},
|
||||
"annotations": [
|
||||
{
|
||||
"@type": "spdx:Annotation",
|
||||
"annotationType": "spdx:REVIEW",
|
||||
"annotator": {
|
||||
"@type": "spdx:Tool",
|
||||
"name": "speckl-compile"
|
||||
},
|
||||
"timestamp": "2026-06-30T02:09:28.194Z",
|
||||
"comment": "Review type: manual"
|
||||
}
|
||||
]
|
||||
},
|
||||
"builtBy": [],
|
||||
"relationships": [
|
||||
{
|
||||
"from": {
|
||||
"@id": "spdx:RetryHandler"
|
||||
},
|
||||
"to": {
|
||||
"@id": "spdx:speck:FaultHandler"
|
||||
},
|
||||
"relationshipType": "spdx:derivesFrom"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"@id": "spdx:RetryHandler"
|
||||
},
|
||||
"to": {
|
||||
"@id": "spdx:requirement:RESILIENCE-REQ-01"
|
||||
},
|
||||
"relationshipType": "speckl:satisfies",
|
||||
"additionalProperties": [
|
||||
{
|
||||
"name": "speckl:clause",
|
||||
"value": "3.1.2"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"@id": "spdx:RetryHandler"
|
||||
},
|
||||
"to": {
|
||||
"@id": "spdx:requirement:RESILIENCE-REQ-02"
|
||||
},
|
||||
"relationshipType": "speckl:satisfies"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"@id": "spdx:RetryHandler"
|
||||
},
|
||||
"to": {
|
||||
"@id": "spdx:agent:Scott Scoble"
|
||||
},
|
||||
"relationshipType": "spdx:wasAttributedTo"
|
||||
},
|
||||
{
|
||||
"from": {
|
||||
"@id": "spdx:RetryHandler"
|
||||
},
|
||||
"to": {
|
||||
"@id": "spdx:source:architecture_review:AR-2024-0215"
|
||||
},
|
||||
"relationshipType": "spdx:wasInformedBy"
|
||||
}
|
||||
],
|
||||
"elements": [
|
||||
{
|
||||
"@type": "spdx:Requirement",
|
||||
"@id": "spdx:requirement:RESILIENCE-REQ-01",
|
||||
"name": "RESILIENCE-REQ-01"
|
||||
},
|
||||
{
|
||||
"@type": "spdx:Requirement",
|
||||
"@id": "spdx:requirement:RESILIENCE-REQ-02",
|
||||
"name": "RESILIENCE-REQ-02"
|
||||
},
|
||||
{
|
||||
"@type": "spdx:Person",
|
||||
"@id": "spdx:agent:Scott Scoble",
|
||||
"name": "Scott Scoble",
|
||||
"email": "scott@scoble.me"
|
||||
},
|
||||
{
|
||||
"@type": "spdx:Activity",
|
||||
"@id": "spdx:source:architecture_review:AR-2024-0215",
|
||||
"name": "architecture_review - AR-2024-0215",
|
||||
"type": "architecture_review"
|
||||
}
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue