lucydsl / liblucy Goto Github PK
View Code? Open in Web Editor NEWCore Lucy compiler
Home Page: https://pkg.spooky.click/lucylang/
Core Lucy compiler
Home Page: https://pkg.spooky.click/lucylang/
The compiler appears to be breaking when you use a _
in an event name.
I'm thinking that having nested states by nesting a machine is kind of strange. It's like a machine but also is completely tied to the outer. Does it make sense to just have states directly inside of states instead?
import { parseToJson } from '@lucylang/node';
const machine = parseToJson(
`
// Lucy file in here
`
)
Many use-cases for this - JS runtime, browser-based tooling, all sorts of cool stuff.
Currently the delay
token compiles to the delay
property in the XState configuration result.
delay
should be compiled to after
.
Input:
initial state green {
delay(1s) => yellow
}
state yellow {
delay(500) => red
}
state red {
delay(calcLightDelay) => green
}
Output:
import { Machine } from 'xstate';
export default Machine({
initial: 'green',
states: {
green: {
after: {
1000: 'yellow'
}
},
yellow: {
after: {
500: 'red'
}
},
red: {
after: {
calcLightDelay: 'green'
}
}
}
}, {
delays: {
calcLightDelay: 3000
}
});
This would be great for building tools with Lucy - for instance a browser-based visualiser.
Hi!! Thanks for you work!! It's been fun playing with Lucy ๐
I was thinking would it be possible to generate Lucy files from XState JSON definitions? Not sure if it would be super complex or is something simple.
I thought about it as I was replicating my XState machines in Lucy manually :)
Thanks!
I'm trying to follow the example from Lucy's website for using an "use reference" in invoke statement. I'm quite new to state machines in general so my terminology and expectations might be incorrect here - feel free to correct me.
Input:
use './utils' { getUsers }
state idle {
invoke(:getUsers) {
done => assign(users)
}
}
Output with @lucy/[email protected]
:
01 | import { createMachine, assign } from 'xstate';
02 | import { getUsers } from './utils';
03 |
04 | export default function({ context = {}, services } = {}) {
05 | return createMachine({
06 | context,
07 | states: {
08 | idle: {
09 | invoke: {
10 | src: 'getUsers',
11 | onDone: {
12 | actions: [
13 | assign({
14 | users: (context, event) => event.data
15 | })
16 | ]
17 | }
18 | }
19 | }
20 | }
21 | }, {
22 | services: {
23 | getUsers: services.getUsers
24 | }
25 | });
26 | }
Note that on the line 2 the imported getUsers
is an unused variable. It is not used on line 23.
I would expected the output to be:
22 | services: {
23 | getUsers,
24 | }
or maybe even
22 | services: {
23 | getUsers,
24 | ...services,
25 | }
module.exports = function(content) {
const callback = this.async();
import('@lucy/liblucy')
.then(({ ready, compileXstate }) => {
ready
.then(() => {
const { js } = compileXstate(content, this.resourcePath);
callback(undefined, applyFixes(js));
})
.catch(e => {
callback(
`Lucy loader failed to compileXstate: ${e.message}`
);
});
})
.catch(callback);
};
function applyFixes(input) {
const output = input.replace(/services\./gi, '');
console.log('input', input);
console.log('output', output);
return output;
}
Hey,
Noticed that if you try to make a transition containing only an assign action, it adds a "ssign"
target
if no other state is defined, or ""
if another state is defined (whereas I would expect no target
in such a case)
state initial {
TEST => assign(someValue)
}
The resulting code is
import { createMachine, assign } from 'xstate';
export default createMachine({
states: {
initial: {
on: {
TEST: {
target: 'ssign',
actions: [
assign({
someValue: (context, event) => event.data
})
]
}
}
}
}
});
And when doing the following:
state initial {
TEST => assign(someValue)
}
state other {}
The resulting code is
import { createMachine, assign } from 'xstate';
export default createMachine({
states: {
initial: {
on: {
TEST: {
target: '',
actions: [
assign({
someValue: (context, event) => event.data
})
]
}
}
},
other: {
}
}
});
I'm aware that Lucy is in alpha and issues are to be expected, just wanted to give you a heads up :)
Good luck!
I think that i found a bug in lucy 0.3.4
use './utils.js' {assignDays}
machine testMachine {
action assignDays = assignDays
initial state test {}
}
compiles to
import { createMachine } from 'xstate';
import { assignDays } from './utils.js';
export function createTestMachine({ context = {} } = {}) {
return createMachine({
initial: 'test',
context,
states: {
test: {
}
}
}, {
actions: {
assignDays: assignDays
}
});
}
which is ok, but
use './utils.js' {assignDays}
machine testMachine {
action assignDays = assignDays
initial state test {
machine nested {
}
}
}
compiles to
import { createMachine } from 'xstate';
import { assignDays } from './utils.js';
export function createTestMachine({ context = {} } = {}) {
return createMachine({
initial: 'test',
context,
states: {
test: {
context
}
}
});
}
where actions dissapeared
I believe I've isolated this down. In the tableau
state there are multiple repeated up
and down
transitions with different given guards. If you delete any two of those event transitions it'll correctly compile. With three or more present it'll hang the compiler.
machine highlighted {
guard isUnderStock = :isUnderStock
guard isUnderWaste = :isUnderWaste
guard isUnderFoundation = :isUnderFoundation
initial state stock {
up => tableau
down => tableau
left => foundation
right => waste
}
state foundation {
up => tableau
down => tableau
left => guard(:isStartOfFoundation) => waste
left => foundation
right => guard(:isEndOfFoundation) => stock
right => foundation
}
state waste {
up => tableau
down => tableau
left => stock
right => foundation
}
state tableau {
up => isUnderStock => stock
up => isUnderWaste => waste
up => isUnderFoundation => foundation
up => tableau
down => isUnderStock => stock
down => isUnderWaste => waste
down => isUnderFoundation => foundation
down => tableau
left => tableau
right => tableau
}
}
For example:
state error {
@entry => action(:log)
}
guard isValid = isValid
initial state firstState {
=> isValid => secondState
}
state secondState {
}
This appears not to print anything into the cond of the always
use './util' { logger }
action log = logger
state idle {
go => assign(prop) => log
}
Compiles to:
import { createMachine, assign } from 'xstate';
import { logger } from './util';
export default createMachine({
states: {
idle: {
on: {
go: {
actions: [
assign({
prop: (context, event) => event.data
}), 'log'
]
}
}
}
}
}, {
actions: {
log: logger
}
});
'log'
should be on a newline
Hi there, this project looks great with super exciting potential :D
I came straight to the github, keen to see juicy implementation details, only to find it written in C, a language that I am generally not good at and find hard to read (and not buildable on windows?) :( Nothing wrong with C of course... I am just curious though, why not typescript / PEGjs for the core? Is javascript really too slow for this?
initial state firstState {
go => guard(isAllowed) => secondState
}
state secondState {}
state notAllowedState {}
Given a state machine like the one above, how would one default to going to the notAllowedState
if isAllowed
evaluated to false?
compiler: () => spawn(services.compiler, 'compiler')
Instead of:
compiler: spawn(services.compiler, 'compiler')
use './util' { logger }
machine one {
action log = logger
state idle {
go => log
}
}
machine two {}
Compiles to:
import { createMachine } from 'xstate';
import { logger } from './util';
export const one = createMachine({
states: {
idle: {
on: {
go: {
actions: ['log']
}
}
}
}
}, {
actions: {
log: logger
}
});
export const two = createMachine({
}, {
actions: {
log: logger
}
});
Here two
should not have the logger.
Probably through a modifier like with final/initial.
parallel machine File {
}
Should be something like this:
send((ctx, ev) => ({ type: 'compile', data: ev.data }), {
to: (context) => context.compiler
})
Congrats on the new DSL! It is indeed a significant improvement on the experience of writing large-enough state machines (e.g., using state machines where they do make sense). I have two questions:
Got to look at your code anyways to see how you did it but thought I'll stop by and ask.
[edit]: Ok, I had a quick look and it seems like I should be able to reuse your parser. I should just have to rewrite the traversal of the parsed tree (ParseResult
). Does that make sense to you? BTW that is terrificly well-written C code. I haven't read C in a decade, and that went like a charm even without comments :-)
Is there a runtime for this as well?
Similar to how in graphql you can create documents by using the 'gql' function, would it be possible to create state machines in the same way?
I saw a twitter post too with this exact approach.
https://twitter.com/n_moore/status/1385986489797029894?s=21
I look forward to this project's growth ๐
Sometimes, actions/services/guards are best implemented not in the initial definition of the machine. Sometimes, it's best to leave them unimplemented until you use the machine in your frontend/backend code.
This means that Lucy would need to compile action definitions that aren't imported via use
.
I think it would be better to use XState's createMachine as it's the recommended syntax and will be the only option on XState v5 :)
I'm really interested in trying Lucy! Thanks for your work.
I tried to do what the install page says on my terminal https://lucylang.org/install/:
curl -sSf https://lucylang.org/install.sh | bash
It went ok!
Then did
lc --help
But I get
zsh: command not found: lc
Checked my .zshrc config and it automatically added a
export PATH="/Users/santicros/.lucy/bin:$PATH"
I found that removing the final $PATH made it work, so:
export PATH="/Users/santicros/.lucy/bin"
:D
There appears not to be a lucy-native way for stopping spawned actors. Pointed out by @VanTanev on a recent stream of mine.
Opening this issue to spawn a discussion about type-safety. Lucy offers a nice opportunity to increase TS type safety by statically analysing the machine. This is similar to, but much more reliable than, xstate-codegen. xstate-codegen
pulls out machines from JS/TS, which is fraught with issues. Lucy could statically analyse files, free of side effects, and create 100% perfect types every time.
What do perfect types look like?
state.matches
can be made type safe.Because Lucy compiles to out.js
files, an accompanying .d.ts
file would be appropriate. Users could also pass it generics, such as TEvent
and TContext
, similar to xstate-codegen
.
Interested to hear your thoughts.
Comments using:
// comment
...appear not to be possible. This would be great, and also support the examples in the docs.
Given a machine that looks like this:
initial state idle {
fetch => fetching
machine idling {
initial state noError {
@entry => action(:clearErrorMessage)
}
state errored {}
}
}
state fetching {
reportError => idle.errored
}
I would like to target a nested state. Something like reportError => idle.errored
.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.