Skip to content

Add TCL support for WASM builds#519

Open
Intubun wants to merge 14 commits into
RTimothyEdwards:masterfrom
Intubun:master
Open

Add TCL support for WASM builds#519
Intubun wants to merge 14 commits into
RTimothyEdwards:masterfrom
Intubun:master

Conversation

@Intubun
Copy link
Copy Markdown
Contributor

@Intubun Intubun commented May 22, 2026

Hey everyone,
ive been working on adding tcl support to the magic-vlsi-wasm package. Ive added:

  • A WASM build variant alongside existing non-TCL build
  • tcl/tk gets compiled as static library and linked directly into magic.wasm
  • new --with-tk=no configure path for TCL without Tk builds, since WASM only runs headless
  • CI workflow for all these changes
  • More memory by standard (64 MB for heap and 10 MB for Stack, can be adjusted at runtime)

Matrix and all CI already ran on my fork and everything is green.

Intubun and others added 13 commits May 21, 2026 12:42
Bump VERSION to 8.3.645.

magic.wasm can now be built as two variants packaged in the same npm
release: notcl/ (legacy, magic's own parser) and tcl/ (intubun/tcl 9.x
statically linked, commands evaluated by Tcl_EvalEx). The TCL fork is
pinned via npm/tcl.ref and cloned/built by magic itself — the tcl/
checkout is treated as read-only and built out-of-source into
magic/build-tcl-wasm/.

Configure layer:
- New usingTk variable decoupled from usingTcl in scripts/configure.in
  + scripts/configure, so --with-tcl --without-tk is finally a valid
  combination. Native Linux Tcl+Tk builds keep their previous behaviour
  (both flags default to enabled).
- When usingTk is empty, configure passes -DMAGIC_NO_TK so the small
  number of remaining Tk callsites in tcltk/tclmagic.{h,c} compile out,
  and TKCOMMON_SRCS / USE_TK_STUBS are omitted from the link.

WASM build orchestration:
- toolchains/emscripten/build-tcl-wasm.sh builds libtcl9.x.a + libtclstub.a
  + tclConfig.sh out-of-source from a pristine intubun/tcl checkout.
- npm/build.sh grew a --variant=<tcl|notcl|both> flag and writes its
  outputs into npm/tcl/ and npm/notcl/. It also clones intubun/tcl with
  autocrlf=false at the SHA pinned by npm/tcl.ref.
- magic/Makefile (WASM block only): magicWasm.o is now compiled with
  DFLAGS_NOSTUB so Tcl_CreateInterp resolves to libtcl9.x directly
  before tclStubsPtr is set. magic.js link pulls in LIB_SPECS_NOSTUB
  and -ltclstub. After rules.mak include, magic: is a phony alias for
  magic.js so the generic ${MODULE} recipe doesn't fight it.
- toolchains/emscripten/defs.mak: add -sUSE_ZLIB=1 (libtcl9 references
  zlib), replace -sSTACK_SIZE=N with -Wl,-z,stack-size=N (emcc >=5
  rejects the setting form).
- magic/magicWasm.c bootstraps the embedded interp under MAGIC_WRAPPER
  (Tcl_CreateInterp -> Tcl_Init -> Tclmagic_Init) and routes
  run_command through Tcl_EvalEx.
- magic/magicTop.c: gate MagicVersion/Revision/CompileTime on
  !MAGIC_WRAPPER so they don't collide with the copies in
  tcltk/tclmagic.c when both objects land in the same wasm binary.

npm package:
- Subpath exports: ".", "./tcl", "./notcl". Default import keeps the
  pre-existing non-TCL behaviour for backward compatibility.
- examples/smoke-tcl.mjs exercises the TCL variant.

CI:
- main-wasm.yml clones intubun/tcl at the pinned ref, builds both
  variants via npm/build.sh --variant=both, runs the existing notcl
  test suite and the new TCL smoke test, and publishes only on a
  v<x.y.z>... git tag. Tag name (minus the leading v) becomes the
  npm version.
…ompletes

magic_wasm_init() called Tclmagic_Init() and magicMainInit() but never
ran the command-registration loop in _magic_initialize(), so magic::tech,
magic::load, magic::gds and all other Magic commands were missing from
the Tcl interpreter.

Add TclmagicRegisterCommands() in tclmagic.c containing the
WindNextClient/WindGetCommandTable loop and call it from magic_wasm_init()
after magicMainInit() succeeds.

Also change magic_wasm_source_file() to use Tcl_EvalFile in MAGIC_WRAPPER
mode so scripts with magic:: commands are evaluated through the Tcl
interpreter instead of the plain text dispatcher.
All non-TCL tests (extract, gds, drc, cif) now also run against the TCL
variant using magic::-prefixed Tcl scripts. A new PCell test generates
two parameterized rectangle cells and verifies their GDS output.

The CI workflow runs npm run test:tcl after the TCL build and saves the
generated output files as an artifact.
…comments and naming

Rename *-magic.tcl scripts to *-tcl.tcl to match the -tcl.js test naming
convention. Delete the old bare-command pcell.tcl (used cellname create,
no magic:: prefix) and promote pcell-magic.tcl to pcell.tcl.

Fix misleading comment in magicWasm.c: Tclmagic_Init only bootstraps the
interpreter; magic:: commands are registered separately by
TclmagicRegisterCommands after magicMainInit. Align comment block
dashes in TclmagicRegisterCommands to match tclmagic.c style (62 dashes).

Replace intubun/tcl references in build scripts and CI with tcltk/tcl to
match the actual pinned repo in npm/tcl.ref. Also run both test suites
when build.sh is invoked with --test.
Replace the last three intubun/tcl mentions with tcltk/tcl in
npm/tcl.js, toolchains/emscripten/build-tcl-wasm.sh, and npm/tcl.ref.
Also remove the stale comment in tcl.ref that referenced a non-existent
update-tcl GitHub Actions workflow.
tclmagic.c: remove stray /*-----*/ line left over from a previous edit
that left a duplicate comment opener before TclmagicRegisterCommands.

magicWasm.c: move TxSetPoint inside the #else (non-TCL) branch of
magic_wasm_source_file and restore its explanation comment. TxSetPoint
routes TxDispatch commands to the layout window; it is irrelevant and
misleading in the Tcl_EvalFile path.

magic/Makefile: guard the TCL linker flags in the magic.js link rule
with ifneq (${TCL_LIB_DIR},). When building the non-TCL WASM variant
TCL_LIB_DIR is empty, so the unconditional -L${TCL_LIB_DIR} -ltclstub
expanded to a bare -L flag and a missing library, breaking the notcl
build.
… in CI

Add explicit length limit to sscanf in TclmagicRegisterCommands: %92s
instead of %s prevents a potential stack overwrite if a command name
were ever longer than the buffer. Matches the available space (keyword[100]
minus the 7-byte "magic::" prefix minus null).

Extend the CI output-display step to also iterate over output-tcl/ so
that TCL-variant test regressions are visible in the job log without
downloading artifacts.
Copy link
Copy Markdown
Collaborator

@dlmiles dlmiles left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A quick skim through for some initial feedback.

Comment thread npm/build.sh Outdated

if [ "$variant" = "tcl" ]; then
ensure_tcl_built
CFLAGS="--std=c17 -D_DEFAULT_SOURCE=1 -DEMSCRIPTEN=1${EXTRA_CFLAGS}" \
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

-DEMSCRIPTEN=1${EXTRA_CFLAGS}

Is this correct without a space before the dollar ?

There is at least one more site below (please check all the files in npm/**). This may appear to work when EXTRA=CFLAGS="" but if you configure it to a non-blank string I would expect things to error.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had the spaces at other places and therefore it compiled correctly, but yeah, Ill fix it.

Comment thread npm/README.md

```bash
# Override the default clone location
TCL_REPO=/path/to/existing/tcl bash npm/build.sh --variant=tcl
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not clear what the function of this part is. If the WASM TCL is built, you would expect the C TCL to be installed as well. So there is a $prefix/lib/tclConfig.sh which should contain all the information a dependant project needs to consume Tcl. This config pre-dates pkg-config system that, but serves the same purpose.

If this part is for making the Tcl source available to build the WASM target. This is probably best considered being done entirely inside Tcl. For example the 'unix' subdir is for UNIX targets, the 'win' subdir is for Win32 targets, etc... I'm sure Tcl upstream project would welcome having a contribution with a 'wasm' subdir that builts Tcl for a WASM target, possibly with access to WASI (which is the closest APIs suite that should cover core functionality).

When maybe a shim for WASM in the browser. To manage WASI and non-WASI and this would be a more minor target adjustment.

This comment should not hold up the contribution here, just feedback that access to a working Tcl on WASM target might be something more Tcl users want and sits well with the construction of the project in a wasm/ subdir.

Comment thread npm/tcl.ref Outdated
# 3. Commit + push. CI rebuilds and republishes.

TCL_REPO_URL=https://github.com/tcltk/tcl.git
TCL_REF=84b23291b0dd811d642abef4ec7a55473c3eccb3
Copy link
Copy Markdown
Collaborator

@dlmiles dlmiles May 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TCL_REF=main

Is there a reason this is pinned by default ? Ideally should this not take the latest tag of the stable branch. So by default CI will also rebuild against newer TCL.

Can the build use the environment $TCL_REF in priority to the default here, "refs/tags/9.0.3"

Can the build use the environment $TCL_REPO_URL in priority to this default here.

Can release notes be constructed around the GitHub artifact creation, so at the bottom of the workflow task. The goal is to at a glance see the versions of important things that were use by the CI.

This might be compiler, emscripten, tcl commit/tag/url.

Ideally the CI should be optimistically building for newer current stuff in low maintenance mode by default, so no one need to update the SHA here.

Maybe in this case when the GitHub workflow runs, one task is a form of information gathering, maybe to decide the TCL_REF value and report in summary the git log -n1 style output. It then sets $TCL_REF in the workflow, to this value here can be set to TCL_REF=main. The information gathering is then used to render a $GITHUB_STEP_SUMMARY to display things with the *.wasm downloads.

If the CI requires pinning and heuristics over picking a working version to use, the pinning would be done in the workflow file.

This would then extend to using on:workflow_dispatch to specify a specify TCL_REF.

All this is to help in 6 months when it breaks to have as much information as possible available in CI to spend the least amount of time rebuilding magic using the previously working TCL_REF to confirm the problem is with the current state of the upstream project, not with magic's recent changes.

Part of the reason why the CI workflow default TCL_REF should be computed on the most recent stable branch main release tag, is only use Tcl versions that the Tcl project consider release ready.

If you are locking a version (because that is needed) around the SHA please show the git log -n1 info nearby, to show the commit vital statistics (date/tag/author/comment).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ive deleted the tcl.ref and now the tcl verison is tagged to the latest stable release. You can override the verion with a manual workflow dispatch. Also a build summary was added.

@dlmiles
Copy link
Copy Markdown
Collaborator

dlmiles commented May 22, 2026

Can we resolve the Semantic Version scheme for NPM package versions as well ?

Comment added after close #512 (comment)

Specifically what is bad is the use of a raw git hash after a decimal point such as example: 8.3.799-20260622.01234cdef. This as I understand it, is the scheme used in the first release.

It is better to be 8.3.799.20260622git0123cdef where git seems to be the defacto standard in the NPM package world for this usage? as it conveys what the sequence of the following characters means ? if you recall the goal is to allow insertion of security updates (if the project wanted to) after the events in the future. The goal would be some one specifying they want magic==8.3.799 would get the latest version in the 8.3.799 series, thus picking up an inserted security update. Each part after decimal point should follow an order (as per semantic versioning), but use of the githash as .01234cdef breaks this as the next hash might be .0fedc321 but that now appears to be before.

Not thinking about this in advance, makes it impossible to fix in the future.
In practice the project may just request users to upgrade to the latest release, but having the option is always best planned for in advance.

I just have not done to research (myself) to see how other NPM projects manage including the git-hash with semantic versioning. But I know that they do manage this within their visioning scheme. But what I can see at this time is exactly what I didn't want.

@Intubun
Copy link
Copy Markdown
Contributor Author

Intubun commented May 22, 2026

There are two distinct version formats in this workflow worth separating:

Published releases are tag-driven: pushing git tag v8.3.799 causes the workflow to publish exactly 8.3.799 to GitHub Packages — clean three-component semver, no suffix. Security patches would be published as 8.3.800, 8.3.801 etc. Users who declare ~8.3.799 in their package.json receive those automatically on npm update. This is the standard npm mechanism for the use case described and is already in place.

CI snapshots (non-tag builds) are packed as artifacts for local inspection but are never published to GitHub Packages. The format has been updated from 8.3.799-20260622.01234cdef to 8.3.799+20260622.git01234cdef. The original - was the real problem: in semver it marks a pre-release, meaning the snapshot sorted before 8.3.799 rather than after. The + separator correctly marks it as build metadata ("this is 8.3.799 built from this commit"), and the git prefix makes the hash immediately recognisable. Since snapshots are never published, the hex ordering concern does not arise in practice.

Regarding 8.3.799.20260622git0123cdef — the intent is right but npm's node-semver strictly requires exactly three numeric components and rejects a fourth, so this format cannot be used regardless of convention.

@dlmiles
Copy link
Copy Markdown
Collaborator

dlmiles commented May 22, 2026

The upgrade to latest is always an option, and not the point I am drawing attention to (as it is an obvious matter).

The security update means keep the same 8.3.799 version but receive a security patch on top (the version needs to be inserted into the range). For example there maybe users who lock a specific version of magic because it has been tested/checked and because they are building chips (with a 1 year lifecycle and expensive stability concerns) who want stability not new features, so they would use magic==8.3.799 because it passed their internal processes at some point in the past.

The idea of a security (or major bugfix/correction) patch issue in this way is to insert it into the range 8.3.799 range after 8.3.800 was release (retrospectively).

Is it allowed in semantic versioning to have 8.3.799.1+20260622git0123cdef and will that version always be considered a match for magic==8.3.799 when the original release was 8.3.799+20260622git0123cdef. By using the + it seems like that manages the extra part, and by not using . we can still have a 4th part to the version to insert the security update ?


Ah then if it strictly only allows 3 parts to a version, I am saying the main release also need the +20260622git0123cdef part to allow the security/major-fix to be inserted.

@Intubun
Copy link
Copy Markdown
Contributor Author

Intubun commented May 22, 2026

npm does treat any three part sementic versioning the same, no matter the extension. npm, correct me if im wrong, does not have any way to retroactively insert security patches. The only path for patching is to increase the version number. The part after that is only for traceability. At least thats my knowledge on how the npm registry handles it.

@dlmiles
Copy link
Copy Markdown
Collaborator

dlmiles commented May 22, 2026

I shall research and test some things over the new few days.

Is there any reason why ${base}-${date}+git${hash} is a bad idea ? This would seem to address all constraints:

  • NPM semver works, as 3 parts, a + introduces supplementary information.
  • An update for the product version 8.3.799 can be inserted, as 799-20260522 is older than 799-20260801
  • The release build would use versioning in the form 8.3.799-20260522 due to this.
  • Also ${base}~${date}+git${hash} would be ok, if in NPM world use if tilde '~' is a defaco standard.

Unclear if magic>=8.2.799 matches.

As I understand it from your comments: a . after a + has a different meaning (it is not part of semver matching), but in the above scheme is was removed anyway, side-stepping the concern, but in NPM and the casual observer (who does not know/understand semver rules).

I don't see having the date on display being a bad thing, a human reading can immediately make a judgement call from seeing a date on they should consider to update, based around all the information they know from their view of the world.

@Intubun
Copy link
Copy Markdown
Contributor Author

Intubun commented May 22, 2026

yeah semver matching is different depending on the symbol. ".", "-" and "+" have different meanings. "." is for sectioning the main version, "-" can be used to declare alpha / beta version. "1.1.1-alpha.1" for example. and "+" only adds additional information, for example build time information, etc. But I should also do more research on the topic

@Intubun
Copy link
Copy Markdown
Contributor Author

Intubun commented May 26, 2026

I did my research, the "-" has to be actively selected by the user, since its only intended for prerelease version. Its mostly intended to implement patches by bumping the last part of the version. for example: magic>=8.3.799 would not match with 8.3.799-20260526, since its considered a prerelease version of a package. So Users would not get the update, unless they actively pin that version with magic@8.3.799-20260526, but it wont update the package automatically with the next npm install / npm update. And since @RTimothyEdwards magic repo is mirrored by github every day at around 9am my time, there is a more or less daily release schedule and if we ship it just as 8.3.799, then all the users would get the newest version, if they run npm install or update.

@dlmiles
Copy link
Copy Markdown
Collaborator

dlmiles commented May 27, 2026

My research gets to here.

Using a version in the form: 8.3.799020261231+git0123cdef

The inserted ZERO just makes the date easier to read. Can also be used for .1 to .9 style 4th part, the point is the scheme leaves the option open to decide another day. Which is what thinking about it now is all about (creating future options).

Version locking can be done in a number of ways, see below. <8.3.800xxxxxxxxx

VALID 8.3.799~20261231+git0123cdef -> false
VALID 8.3.799_20261231+git0123cdef -> false
VALID 8.3.799$20261231+git0123cdef -> false
VALID 8.3.799%20261231+git0123cdef -> false
VALID 8.3.799^20261231+git0123cdef -> false
VALID 8.3.799&20261231+git0123cdef -> false
VALID 8.3.799*20261231+git0123cdef -> false
VALID 8.3.799(20261231+git0123cdef -> false
VALID 8.3.799)20261231+git0123cdef -> false
VALID 8.3.799=20261231+git0123cdef -> false
VALID 8.3.799[20261231+git0123cdef -> false
VALID 8.3.799]20261231+git0123cdef -> false
VALID 8.3.799{20261231+git0123cdef -> false
VALID 8.3.799}20261231+git0123cdef -> false
VALID 8.3.799#20261231+git0123cdef -> false
VALID 8.3.799@20261231+git0123cdef -> false
VALID 8.3.799:20261231+git0123cdef -> false
VALID 8.3.799;20261231+git0123cdef -> false
VALID 8.3.799.20261231+git0123cdef -> false
VALID 8.3.799,20261231+git0123cdef -> false
VALID 8.3.799>20261231+git0123cdef -> false
VALID 8.3.799<20261231+git0123cdef -> false
VALID 8.3.799/20261231+git0123cdef -> false
VALID 8.3.799?20261231+git0123cdef -> false
VALID 8.3.799|20261231+git0123cdef -> false
VALID 8.3.799020261231+git0123cdef -> true
VALID 8.3.799020261231+git0123cdef -> true
VALID 8.3.999020261231+git0123cdef -> true
VALID 8.3.1000020261231+git0123cdef -> true
VALID 8.3.1001020261231+git0123cdef -> true
COMPARE 8.3.799 vs 8.3.799020261231+git0123cdef -> -1 BELOW
COMPARE 8.3.800 vs 8.3.799020261231+git0123cdef -> -1 BELOW
COMPARE 8.3.799020261231+git01230000 vs 8.3.799020261231+git0123cdef -> 0 EQUAL
COMPARE 8.3.799020261231+gitffff0000 vs 8.3.799020261231+git0123cdef -> 0 EQUAL
COMPARE 8.3.799020260831+git0123cdef vs 8.3.799020261231+git0123cdef -> -1 BELOW
COMPARE 8.3.799020270101+git0123cdef vs 8.3.799020261231+git0123cdef -> 1 ABOVE
COMPARE 8.3.999020270101+git0123cdef vs 8.3.799020261231+git0123cdef -> 1 ABOVE
COMPARE 8.3.1000020270101+git0123cdef vs 8.3.799020261231+git0123cdef -> 1 ABOVE
COMPARE 8.3.1001020270101+git0123cdef vs 8.3.799020261231+git0123cdef -> 1 ABOVE
MATCH version=8.3.799020261231+git0123cdef range=~8.3.799 -> true
MATCH version=8.3.800020261231+git0123cdef range=~8.3.799 -> true
MATCH version=8.3.1000020261231+git0123cdef range=~8.3.799 -> true
MATCH version=8.3.1001020261231+git0123cdef range=~8.3.799 -> true
MATCH version=8.3.799020261231+git0123cdef range=~8.3.7990 -> true
MATCH version=8.3.800020261231+git0123cdef range=~8.3.7990 -> true
MATCH version=8.3.1000020261231+git0123cdef range=~8.3.7990 -> true
MATCH version=8.3.1001020261231+git0123cdef range=~8.3.7990 -> true
MATCH version=8.3.799020271201+git0123cdef range=~8.3.799 -> true
MATCH version=8.3.799020270101+git0123cdef range=~8.3.799 -> true
MATCH version=8.3.799020270202+git0123cdef range=~8.3.799 -> true
MATCH version=8.3.799020270303+git0123cdef range=~8.3.799 -> true
MATCH version=8.3.800020270101+git0123cdef range=<=8.3.799900000000 -> false
MATCH version=8.3.801020270202+git0123cdef range=<=8.3.799900000000 -> false
MATCH version=8.3.801020270202+git0123cdef range=<=8.3.7999x0000000 -> false
MATCH version=8.3.801020270202+git0123cdef range=<=8.3.7999xxxxxxxx -> false
MATCH version=8.3.799020271201+git0123cdef range=~8.3.7990 -> true
MATCH version=8.3.799020270101+git0123cdef range=~8.3.7990 -> true
MATCH version=8.3.799020270202+git0123cdef range=~8.3.7990 -> true
MATCH version=8.3.799020270303+git0123cdef range=~8.3.7990 -> true
MATCH version=8.3.800020270101+git0123cdef range=<8.3.8000xxxxxxxx -> false
MATCH version=8.3.801020270202+git0123cdef range=<8.3.8000xxxxxxxx -> false
MATCH version=8.3.801020270202+git0123cdef range=<8.3.800xxxxxxxxx -> false

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants