Commitb4e0ac51Recorded12 Jan 2026Repositorysigil-version

sigil-version: Move (sigil version) to dedicated package

Message

The version module provides semver parsing, comparison, and constraints. It's only used by build/release tooling (sigil-build, sigil-changes), not general applications, so it doesn't belong in stdlib.

Changes: - Create packages/sigil-version with the module - Add sigil-version dependency to sigil-build and sigil-changes - Update Makefile: add VERSIONPKG, remove SQLITEPKG (not needed for bootstrap) - Fix stdlib-modules list in sigil-publish docs (add missing modules: array, dict, exceptions, fn, list, profile, random, seq) - Add API review in notes/review/sigil-version.md

Changed
 package.sgl           |  15 ++++++
 src/sigil/version.sgl | 470 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 485 insertions(+)
Diff
package.sgladded
@@ -0,0 +1,15 @@
+1
;;; sigil-version - Semantic Versioning
+2
;;;
+3
;;; Parse, compare, and check version constraints using semantic versioning
+4
;;; (semver 2.0.0). Useful for package management and compatibility checks.
+5
+6
(package
+7
name: "sigil-version"
+8
version: "0.1.0"
+9
description: "Semantic versioning parsing and comparison"
+10
url: "https://codeberg.org/sigil/sigil"
+11
license: "BSD-3-Clause"
+12
authors: (list "David Wilson <[email protected]>")
+13
+14
dependencies: (list
+15
(from-workspace name: "sigil-stdlib")))
src/sigil/version.sgladded
@@ -0,0 +1,470 @@
+1
;;; (sigil version) - Semantic Versioning
+2
;;;
+3
;;; Parse, compare, and check version constraints using semantic versioning
+4
;;; (semver 2.0.0). Useful for package management and compatibility checks.
+5
;;;
+6
;;; ```scheme
+7
;;; (import (sigil version))
+8
;;;
+9
;;; (define v (parse-version "1.2.3-alpha.1"))
+10
;;; (version-major v) ; => 1
+11
;;; (version<? v (parse-version "1.2.4")) ; => #t
+12
;;;
+13
;;; ;; Check version constraints
+14
;;; (version-satisfies? v ">= 1.0.0") ; => #t
+15
;;; (version-satisfies? v "^1.2.0") ; => #t (compatible)
+16
;;; ```
+17
;;;
+18
;;; ## Version Format
+19
;;;
+20
;;; Versions follow the pattern: `MAJOR.MINOR.PATCH[-PRERELEASE][+BUILD]`
+21
;;;
+22
;;; - **MAJOR**: Incremented for breaking changes
+23
;;; - **MINOR**: Incremented for new features (backwards compatible)
+24
;;; - **PATCH**: Incremented for bug fixes
+25
;;; - **PRERELEASE**: Optional pre-release identifier (e.g., `alpha.1`)
+26
;;; - **BUILD**: Optional build metadata (ignored in comparisons)
+27
;;;
+28
;;; ## Constraint Operators
+29
;;;
+30
;;; | Operator | Example | Meaning |
+31
;;; |----------|---------|---------|
+32
;;; | (none) | `1.2.3` | Exact version |
+33
;;; | `=` | `= 1.2.3` | Exact version |
+34
;;; | `>` | `> 1.2.3` | Greater than |
+35
;;; | `>=` | `>= 1.2.3` | Greater than or equal |
+36
;;; | `<` | `< 1.2.3` | Less than |
+37
;;; | `<=` | `<= 1.2.3` | Less than or equal |
+38
;;; | `^` | `^1.2.3` | Compatible (same major, >= given) |
+39
;;; | `~` | `~1.2.3` | Approximately (same major.minor, >= given) |
+40
;;; | `*` | `*` | Any version |
+41
+42
(define-library (sigil version)
+43
(import (sigil string) ; For string<?, string>?, string-trim
+44
(sigil struct)) ; For define-struct
+45
+46
(export
+47
;; Version record
+48
version version?
+49
version-major version-minor version-patch
+50
version-prerelease version-build
+51
version->string
+52
+53
;; Parsing
+54
parse-version
+55
string->version
+56
+57
;; Comparison
+58
version=?
+59
version<?
+60
version>?
+61
version<=?
+62
version>=?
+63
version-compare
+64
+65
;; Constraints
+66
parse-constraint
+67
version-satisfies?
+68
constraint-satisfies?
+69
+70
;; Utilities
+71
version-compatible?
+72
version-bump-major
+73
version-bump-minor
+74
version-bump-patch)
+75
+76
(begin
+77
+78
;; ============================================================
+79
;; Version Record
+80
;; ============================================================
+81
+82
;;; A semantic version with major, minor, patch, prerelease, and build components.
+83
;;;
+84
;;; ```scheme
+85
;;; (version major: 1 minor: 2 patch: 3)
+86
;;; (version major: 2 minor: 0 patch: 0 prerelease: '("alpha" 1))
+87
;;; (version major: 1 minor: 0 patch: 0 build: "20240101")
+88
;;; ```
+89
(define-struct version
+90
;;; Major version number. Breaking changes increment this.
+91
(major default: 0)
+92
;;; Minor version number. New features increment this.
+93
(minor default: 0)
+94
;;; Patch version number. Bug fixes increment this.
+95
(patch default: 0)
+96
;;; Pre-release identifiers as a list of strings or integers, or `#f` for stable releases.
+97
;;; Example: `'("alpha" 1)` for version `1.0.0-alpha.1`
+98
(prerelease default: #f)
+99
;;; Build metadata string, or `#f` if none. Not used in version comparison.
+100
;;; Example: `"20240101"` for version `1.0.0+20240101`
+101
(build default: #f))
+102
+103
;; ============================================================
+104
;; Parsing
+105
;; ============================================================
+106
+107
;;; Parse a version string into a version record.
+108
;;;
+109
;;; Returns `#f` if the string is not a valid version. Accepts optional
+110
;;; leading `v` or `V` prefix.
+111
;;;
+112
;;; ```scheme
+113
;;; (parse-version "1.2.3") ; => <version 1.2.3>
+114
;;; (parse-version "v2.0.0-beta.1") ; => <version 2.0.0-beta.1>
+115
;;; (parse-version "1.0.0+build.42") ; => <version 1.0.0+build.42>
+116
;;; (parse-version "invalid") ; => #f
+117
;;; ```
+118
(define (parse-version str)
+119
(if (not (string? str))
+120
#f
+121
(let* ((str (string-trim str))
+122
;; Strip leading 'v' or 'V' if present
+123
(str (if (and (> (string-length str) 0)
+124
(or (char=? (string-ref str 0) #\v)
+125
(char=? (string-ref str 0) #\V)))
+126
(substring str 1 (string-length str))
+127
str)))
+128
(parse-version-core str))))
+129
+130
;;; Alias for parse-version.
+131
(define string->version parse-version)
+132
+133
;; Internal: Parse the core version string (without leading 'v')
+134
(define (parse-version-core str)
+135
(let* (;; Split off build metadata first (+...)
+136
(build-split (string-split-char str #\+))
+137
(version-part (car build-split))
+138
(build-meta (if (> (length build-split) 1)
+139
(cadr build-split)
+140
#f))
+141
;; Split off prerelease (-...)
+142
(pre-split (string-split-char version-part #\-))
+143
(core-part (car pre-split))
+144
(prerelease-str (if (> (length pre-split) 1)
+145
(string-join (cdr pre-split) "-")
+146
#f))
+147
;; Parse core version numbers
+148
(parts (string-split-char core-part #\.)))
+149
(if (< (length parts) 1)
+150
#f
+151
(let ((major (string->number (car parts)))
+152
(minor (if (> (length parts) 1)
+153
(string->number (cadr parts))
+154
0))
+155
(patch (if (> (length parts) 2)
+156
(string->number (caddr parts))
+157
0)))
+158
(if (and major minor patch
+159
(>= major 0) (>= minor 0) (>= patch 0))
+160
(version
+161
major: major
+162
minor: minor
+163
patch: patch
+164
prerelease: (and prerelease-str
+165
(parse-prerelease prerelease-str))
+166
build: build-meta)
+167
#f)))))
+168
+169
;; Internal: Parse prerelease string into list of identifiers
+170
(define (parse-prerelease str)
+171
(if (or (not str) (string=? str ""))
+172
#f
+173
(map (lambda (id)
+174
(let ((num (string->number id)))
+175
(if num num id)))
+176
(string-split-char str #\.))))
+177
+178
;;; Convert a version to its string representation.
+179
;;;
+180
;;; ```scheme
+181
;;; (version->string (parse-version "1.2.3-alpha"))
+182
;;; ; => "1.2.3-alpha"
+183
;;; ```
+184
(define (version->string v)
+185
(let ((core (string-append
+186
(number->string (version-major v))
+187
"."
+188
(number->string (version-minor v))
+189
"."
+190
(number->string (version-patch v)))))
+191
(let ((with-pre (if (version-prerelease v)
+192
(string-append core "-"
+193
(prerelease->string (version-prerelease v)))
+194
core)))
+195
(if (version-build v)
+196
(string-append with-pre "+" (version-build v))
+197
with-pre))))
+198
+199
;; Internal: Convert prerelease list to string
+200
(define (prerelease->string ids)
+201
(string-join (map (lambda (id)
+202
(if (number? id)
+203
(number->string id)
+204
id))
+205
ids)
+206
"."))
+207
+208
;; ============================================================
+209
;; Comparison
+210
;; ============================================================
+211
+212
;;; Compare two versions.
+213
;;;
+214
;;; Returns `-1` if a < b, `0` if a = b, `1` if a > b.
+215
;;; Build metadata is ignored per semver spec.
+216
;;;
+217
;;; ```scheme
+218
;;; (version-compare (parse-version "1.0.0")
+219
;;; (parse-version "2.0.0")) ; => -1
+220
;;; ```
+221
(define (version-compare a b)
+222
(let ((major-cmp (compare-numbers (version-major a) (version-major b))))
+223
(if (not (= major-cmp 0))
+224
major-cmp
+225
(let ((minor-cmp (compare-numbers (version-minor a) (version-minor b))))
+226
(if (not (= minor-cmp 0))
+227
minor-cmp
+228
(let ((patch-cmp (compare-numbers (version-patch a) (version-patch b))))
+229
(if (not (= patch-cmp 0))
+230
patch-cmp
+231
(compare-prerelease (version-prerelease a)
+232
(version-prerelease b)))))))))
+233
+234
;; Internal: Compare two numbers
+235
(define (compare-numbers a b)
+236
(cond
+237
((< a b) -1)
+238
((> a b) 1)
+239
(else 0)))
+240
+241
;; Internal: Compare prerelease identifiers
+242
(define (compare-prerelease a b)
+243
(cond
+244
((and (not a) (not b)) 0)
+245
((not a) 1) ; no prerelease > prerelease
+246
((not b) -1)
+247
(else (compare-prerelease-ids a b))))
+248
+249
;; Internal: Compare prerelease identifier lists
+250
(define (compare-prerelease-ids a b)
+251
(cond
+252
((and (null? a) (null? b)) 0)
+253
((null? a) -1)
+254
((null? b) 1)
+255
(else
+256
(let ((cmp (compare-prerelease-id (car a) (car b))))
+257
(if (not (= cmp 0))
+258
cmp
+259
(compare-prerelease-ids (cdr a) (cdr b)))))))
+260
+261
;; Internal: Compare two prerelease identifiers
+262
(define (compare-prerelease-id a b)
+263
(cond
+264
((and (number? a) (number? b))
+265
(compare-numbers a b))
+266
((and (string? a) (string? b))
+267
(cond
+268
((string<? a b) -1)
+269
((string>? a b) 1)
+270
(else 0)))
+271
((number? a) -1) ; number < string
+272
(else 1)))
+273
+274
;;; Test if two versions are equal.
+275
;;;
+276
;;; ```scheme
+277
;;; (version=? (parse-version "1.0.0")
+278
;;; (parse-version "1.0.0")) ; => #t
+279
;;; ```
+280
(define (version=? a b)
+281
(= (version-compare a b) 0))
+282
+283
;;; Test if version a is less than version b.
+284
(define (version<? a b)
+285
(< (version-compare a b) 0))
+286
+287
;;; Test if version a is greater than version b.
+288
(define (version>? a b)
+289
(> (version-compare a b) 0))
+290
+291
;;; Test if version a is less than or equal to version b.
+292
(define (version<=? a b)
+293
(<= (version-compare a b) 0))
+294
+295
;;; Test if version a is greater than or equal to version b.
+296
(define (version>=? a b)
+297
(>= (version-compare a b) 0))
+298
+299
;; ============================================================
+300
;; Constraints
+301
;; ============================================================
+302
+303
;;; Parse a version constraint string.
+304
;;;
+305
;;; Returns a constraint record or `#f` if invalid.
+306
;;;
+307
;;; ```scheme
+308
;;; (parse-constraint ">= 1.0.0") ; => (>= <version>)
+309
;;; (parse-constraint "^2.0.0") ; => (caret <version>)
+310
;;; ```
+311
(define (parse-constraint str)
+312
(if (not (string? str))
+313
#f
+314
(let ((str (string-trim str)))
+315
(cond
+316
((string=? str "*")
+317
(list 'any))
+318
((string-prefix? "^" str)
+319
(let ((v (parse-version (substring str 1 (string-length str)))))
+320
(and v (list 'caret v))))
+321
((string-prefix? "~" str)
+322
(let ((v (parse-version (substring str 1 (string-length str)))))
+323
(and v (list 'tilde v))))
+324
((string-prefix? ">=" str)
+325
(let ((v (parse-version (string-trim (substring str 2 (string-length str))))))
+326
(and v (list '>= v))))
+327
((string-prefix? ">" str)
+328
(let ((v (parse-version (string-trim (substring str 1 (string-length str))))))
+329
(and v (list '> v))))
+330
((string-prefix? "<=" str)
+331
(let ((v (parse-version (string-trim (substring str 2 (string-length str))))))
+332
(and v (list '<= v))))
+333
((string-prefix? "<" str)
+334
(let ((v (parse-version (string-trim (substring str 1 (string-length str))))))
+335
(and v (list '< v))))
+336
((string-prefix? "=" str)
+337
(let ((v (parse-version (string-trim (substring str 1 (string-length str))))))
+338
(and v (list '= v))))
+339
(else
+340
(let ((v (parse-version str)))
+341
(and v (list '= v))))))))
+342
+343
;;; Check if a version satisfies a constraint string.
+344
;;;
+345
;;; ```scheme
+346
;;; (version-satisfies? (parse-version "1.5.0") ">= 1.0.0") ; => #t
+347
;;; (version-satisfies? (parse-version "2.0.0") "^1.0.0") ; => #f
+348
;;; ```
+349
(define (version-satisfies? v constraint-str)
+350
(let ((constraint (parse-constraint constraint-str)))
+351
(and constraint (constraint-satisfies? v constraint))))
+352
+353
;;; Check if a version satisfies a parsed constraint.
+354
(define (constraint-satisfies? v constraint)
+355
(case (car constraint)
+356
((any) #t)
+357
((=) (version=? v (cadr constraint)))
+358
((>) (version>? v (cadr constraint)))
+359
((>=) (version>=? v (cadr constraint)))
+360
((<) (version<? v (cadr constraint)))
+361
((<=) (version<=? v (cadr constraint)))
+362
((caret) (caret-satisfies? v (cadr constraint)))
+363
((tilde) (tilde-satisfies? v (cadr constraint)))
+364
(else #f)))
+365
+366
;; Internal: Caret constraint (^X.Y.Z)
+367
(define (caret-satisfies? v base)
+368
(and (version>=? v base)
+369
(cond
+370
((> (version-major base) 0)
+371
(= (version-major v) (version-major base)))
+372
((> (version-minor base) 0)
+373
(and (= (version-major v) 0)
+374
(= (version-minor v) (version-minor base))))
+375
(else
+376
(and (= (version-major v) 0)
+377
(= (version-minor v) 0)
+378
(= (version-patch v) (version-patch base)))))))
+379
+380
;; Internal: Tilde constraint (~X.Y.Z)
+381
(define (tilde-satisfies? v base)
+382
(and (version>=? v base)
+383
(= (version-major v) (version-major base))
+384
(= (version-minor v) (version-minor base))))
+385
+386
;; ============================================================
+387
;; Utilities
+388
;; ============================================================
+389
+390
;;; Check if version a is compatible with version b.
+391
;;;
+392
;;; Two versions are compatible if they have the same major version
+393
;;; and a >= b.
+394
;;;
+395
;;; ```scheme
+396
;;; (version-compatible? (parse-version "1.5.0")
+397
;;; (parse-version "1.2.0")) ; => #t
+398
;;; ```
+399
(define (version-compatible? a b)
+400
(and (= (version-major a) (version-major b))
+401
(version>=? a b)))
+402
+403
;;; Bump the major version (resets minor and patch to 0).
+404
;;;
+405
;;; ```scheme
+406
;;; (version->string (version-bump-major (parse-version "1.2.3")))
+407
;;; ; => "2.0.0"
+408
;;; ```
+409
(define (version-bump-major v)
+410
(version
+411
major: (+ (version-major v) 1)
+412
minor: 0
+413
patch: 0))
+414
+415
;;; Bump the minor version (resets patch to 0).
+416
;;;
+417
;;; ```scheme
+418
;;; (version->string (version-bump-minor (parse-version "1.2.3")))
+419
;;; ; => "1.3.0"
+420
;;; ```
+421
(define (version-bump-minor v)
+422
(version
+423
major: (version-major v)
+424
minor: (+ (version-minor v) 1)
+425
patch: 0))
+426
+427
;;; Bump the patch version.
+428
;;;
+429
;;; ```scheme
+430
;;; (version->string (version-bump-patch (parse-version "1.2.3")))
+431
;;; ; => "1.2.4"
+432
;;; ```
+433
(define (version-bump-patch v)
+434
(version
+435
major: (version-major v)
+436
minor: (version-minor v)
+437
patch: (+ (version-patch v) 1)))
+438
+439
;; ============================================================
+440
;; Internal Utilities
+441
;; ============================================================
+442
+443
(define (string-prefix? prefix str)
+444
(string-starts-with? str prefix))
+445
+446
(define (string-split-char str ch)
+447
(let loop ((chars (string->list str))
+448
(current '())
+449
(result '()))
+450
(cond
+451
((null? chars)
+452
(reverse (cons (list->string (reverse current)) result)))
+453
((char=? (car chars) ch)
+454
(loop (cdr chars)
+455
'()
+456
(cons (list->string (reverse current)) result)))
+457
(else
+458
(loop (cdr chars)
+459
(cons (car chars) current)
+460
result)))))
+461
+462
(define (string-join strs sep)
+463
(if (null? strs)
+464
""
+465
(let loop ((rest (cdr strs))
+466
(result (car strs)))
+467
(if (null? rest)
+468
result
+469
(loop (cdr rest)
+470
(string-append result sep (car rest)))))))))